From 60c4a33fe4fb2fcdb8f590d6bbb47245a8916783 Mon Sep 17 00:00:00 2001 From: "Wei, Jimmy T" Date: Mon, 2 Dec 2024 10:47:22 -0800 Subject: [PATCH 1/7] Updated 2025.0 AI Tools release --- .../JobRecommendationSystem.ipynb | 1386 +++++++++++++++++ .../JobRecommendationSystem.py | 634 ++++++++ .../JobRecommendationSystem/License.txt | 7 + .../JobRecommendationSystem/README.md | 177 +++ .../JobRecommendationSystem/requirements.txt | 10 + .../JobRecommendationSystem/sample.json | 29 + .../third-party-programs.txt | 253 +++ .../IntelJAX_GettingStarted/.gitkeep | 0 .../IntelJAX_GettingStarted/License.txt | 7 + .../IntelJAX_GettingStarted/README.md | 140 ++ .../IntelJAX_GettingStarted/run.sh | 6 + .../IntelJAX_GettingStarted/sample.json | 24 + .../third-party-programs.txt | 253 +++ .../Getting-Started-Samples/README.md | 1 + 14 files changed, 2927 insertions(+) create mode 100644 AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/JobRecommendationSystem.ipynb create mode 100644 AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/JobRecommendationSystem.py create mode 100644 AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/License.txt create mode 100644 AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/README.md create mode 100644 AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/requirements.txt create mode 100644 AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/sample.json create mode 100644 AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/third-party-programs.txt create mode 100644 AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/.gitkeep create mode 100644 AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/License.txt create mode 100644 AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/README.md create mode 100644 AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/run.sh create mode 100644 AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/sample.json create mode 100644 AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/third-party-programs.txt diff --git a/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/JobRecommendationSystem.ipynb b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/JobRecommendationSystem.ipynb new file mode 100644 index 0000000000..2446f11fc8 --- /dev/null +++ b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/JobRecommendationSystem.ipynb @@ -0,0 +1,1386 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "muD_5mFUYB-x" + }, + "source": [ + "# Job recommendation system\n", + "\n", + "The code sample contains the following parts:\n", + "\n", + "1. Data exploration and visualization\n", + "2. Data cleaning/pre-processing\n", + "3. Fake job postings identification and removal\n", + "4. Job recommendation by showing the most similar job postings\n", + "\n", + "The scenario is that someone wants to find the best posting for themselves. They have collected the data, but he is not sure if all the data is real. Therefore, based on a trained model, as in this sample, they identify with a high degree of accuracy which postings are real, and it is among them that they choose the best ad for themselves.\n", + "\n", + "For simplicity, only one dataset will be used within this code, but the process using one dataset is not significantly different from the one described earlier.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GTu2WLQmZU-L" + }, + "source": [ + "## Data exploration and visualization\n", + "\n", + "For the purpose of this code sample we will use Real or Fake: Fake Job Postings dataset available over HuggingFace API. In this first part we will focus on data exploration and visualization. In standard end-to-end workload it is the first step. Engineer needs to first know the data to be able to work on it and prepare solution that will utilize dataset the best.\n", + "\n", + "Lest start with loading the dataset. We are using datasets library to do that." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "saMOoStVs0-s", + "outputId": "ba4623b9-0533-4062-b6b0-01e96bd4de39" + }, + "outputs": [], + "source": [ + "from datasets import load_dataset\n", + "\n", + "dataset = load_dataset(\"victor/real-or-fake-fake-jobposting-prediction\")\n", + "dataset = dataset['train']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To better analyze and understand the data we are transferring it to pandas DataFrame, so we are able to take benefit from all pandas data transformations. Pandas library provides multiple useful functions for data manipulation so it is usual choice at this stage of machine learning or deep learning project.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "rRkolJQKtAzt" + }, + "outputs": [], + "source": [ + "import pandas as pd\n", + "df = dataset.to_pandas()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's see 5 first and 5 last rows in the dataset we are working on." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 556 + }, + "id": "WYGIRBUJSl3N", + "outputId": "ccd4abaf-1b4d-4fbd-85c8-54408c4f9f8a" + }, + "outputs": [], + "source": [ + "df.tail()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, lets print a concise summary of the dataset. This way we will see all the column names, know the number of rows and types in every of the column. It is a great overview on the features of the dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "UtxA6fmaSrQ8", + "outputId": "e8a1ce15-88e8-487c-d05e-74c024aca994" + }, + "outputs": [], + "source": [ + "df.info()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "At this point it is a good idea to make sure our dataset doen't contain any data duplication that could impact the results of our future system. To do that we firs need to remove `job_id` column. It contains unique number for each job posting so even if the rest of the data is the same between 2 postings it makes it different." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 556 + }, + "id": "f4LJCdKHStca", + "outputId": "b1db61e1-a909-463b-d369-b38c2349cba6" + }, + "outputs": [], + "source": [ + "# Drop the 'job_id' column\n", + "df = df.drop(columns=['job_id'])\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And now, the actual duplicates removal. We first pring the number of duplicates that are in our dataset, than using `drop_duplicated` method we are removing them and after this operation printing the number of the duplicates. If everything works as expected after duplicates removal we should print `0` as current number of duplicates in the dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "Ow8SgJg2vJkB", + "outputId": "9a6050bf-f4bf-4b17-85a1-8d2980cd77ee" + }, + "outputs": [], + "source": [ + "# let's make sure that there are no duplicated jobs\n", + "\n", + "print(df.duplicated().sum())\n", + "df = df.drop_duplicates()\n", + "print(df.duplicated().sum())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tcpcjR8UUQCJ" + }, + "source": [ + "Now we can visualize the data from the dataset. First let's visualize data as it is all real, and later, for the purposes of the fake data detection, we will also visualize it spreading fake and real data.\n", + "\n", + "When working with text data it can be challenging to visualize it. Thankfully, there is a `wordcloud` library that shows common words in the analyzed texts. The bigger word is, more often the word is in the text. Wordclouds allow us to quickly identify the most important topic and themes in a large text dataset and also explore patterns and trends in textural data.\n", + "\n", + "In our example, we will create wordcloud for job titles, to have high-level overview of job postings we are working with." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 544 + }, + "id": "c0jsAvejvzQ5", + "outputId": "7622e54f-6814-47e1-d9c6-4ee13173b4f4" + }, + "outputs": [], + "source": [ + "from wordcloud import WordCloud # module to print word cloud\n", + "from matplotlib import pyplot as plt\n", + "import seaborn as sns\n", + "\n", + "# On the basis of Job Titles form word cloud\n", + "job_titles_text = ' '.join(df['title'])\n", + "wordcloud = WordCloud(width=800, height=400, background_color='white').generate(job_titles_text)\n", + "\n", + "# Plotting Word Cloud\n", + "plt.figure(figsize=(10, 6))\n", + "plt.imshow(wordcloud, interpolation='bilinear')\n", + "plt.title('Job Titles')\n", + "plt.axis('off')\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Different possibility to get some information from this type of dataset is by showing top-n most common values in given column or distribution of the values int his column.\n", + "Let's show top 10 most common job titles and compare this result with previously showed wordcould." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 564 + }, + "id": "0Ut0qo0ywv3_", + "outputId": "705fbbf0-4dc0-4ee1-d821-edccaff78a85" + }, + "outputs": [], + "source": [ + "# Get Count of job title\n", + "job_title_counts = df['title'].value_counts()\n", + "\n", + "# Plotting a bar chart for the top 10 most common job titles\n", + "top_job_titles = job_title_counts.head(10)\n", + "plt.figure(figsize=(10, 6))\n", + "top_job_titles.sort_values().plot(kind='barh')\n", + "plt.title('Top 10 Most Common Job Titles')\n", + "plt.xlabel('Frequency')\n", + "plt.ylabel('Job Titles')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can do the same for different columns, as `employment_type`, `required_experience`, `telecommuting`, `has_company_logo` and `has_questions`. These should give us reale good overview of different parts of our dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 564 + }, + "id": "OaBkEWNLxkqK", + "outputId": "efbd9955-5630-4fdb-a6dd-f4ffe4b0a7d8" + }, + "outputs": [], + "source": [ + "# Count the occurrences of each work type\n", + "work_type_counts = df['employment_type'].value_counts()\n", + "\n", + "# Plotting the distribution of work types\n", + "plt.figure(figsize=(8, 6))\n", + "work_type_counts.sort_values().plot(kind='barh')\n", + "plt.title('Distribution of Work Types Offered by Jobs')\n", + "plt.xlabel('Frequency')\n", + "plt.ylabel('Work Types')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 564 + }, + "id": "5uTBPGXgyZEV", + "outputId": "d6c76b5f-25ce-4730-f849-f881315ca883" + }, + "outputs": [], + "source": [ + "# Count the occurrences of required experience types\n", + "work_type_counts = df['required_experience'].value_counts()\n", + "\n", + "# Plotting the distribution of work types\n", + "plt.figure(figsize=(8, 6))\n", + "work_type_counts.sort_values().plot(kind='barh')\n", + "plt.title('Distribution of Required Experience by Jobs')\n", + "plt.xlabel('Frequency')\n", + "plt.ylabel('Required Experience')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For employment_type and required_experience we also created matrix to see if there is any corelation between those two. To visualize it we created heatmap. If you think that some of the parameters can be related, creating similar heatmap can be a good idea." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 696 + }, + "id": "nonO2cHR1I-t", + "outputId": "3101b8b2-cf0a-413b-b0aa-eb2a3a96a582" + }, + "outputs": [], + "source": [ + "from matplotlib import pyplot as plt\n", + "import seaborn as sns\n", + "import pandas as pd\n", + "\n", + "plt.subplots(figsize=(8, 8))\n", + "df_2dhist = pd.DataFrame({\n", + " x_label: grp['required_experience'].value_counts()\n", + " for x_label, grp in df.groupby('employment_type')\n", + "})\n", + "sns.heatmap(df_2dhist, cmap='viridis')\n", + "plt.xlabel('employment_type')\n", + "_ = plt.ylabel('required_experience')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 564 + }, + "id": "mXdpeQFJ1VMu", + "outputId": "eb9a893f-5087-4dad-ceca-48a1dfeb0b02" + }, + "outputs": [], + "source": [ + "# Count the occurrences of unique values in the 'telecommuting' column\n", + "telecommuting_counts = df['telecommuting'].value_counts()\n", + "\n", + "plt.figure(figsize=(8, 6))\n", + "telecommuting_counts.sort_values().plot(kind='barh')\n", + "plt.title('Counts of telecommuting vs Non-telecommuting')\n", + "plt.xlabel('count')\n", + "plt.ylabel('telecommuting')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 564 + }, + "id": "8kEu4IKcVmSV", + "outputId": "94ae873f-9178-4c63-e855-d677f135e552" + }, + "outputs": [], + "source": [ + "has_company_logo_counts = df['has_company_logo'].value_counts()\n", + "\n", + "plt.figure(figsize=(8, 6))\n", + "has_company_logo_counts.sort_values().plot(kind='barh')\n", + "plt.ylabel('has_company_logo')\n", + "plt.xlabel('Count')\n", + "plt.title('Counts of With_Logo vs Without_Logo')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 564 + }, + "id": "Esv8b51EVvxx", + "outputId": "40355e3f-fc6b-4b16-d459-922cfede2f71" + }, + "outputs": [], + "source": [ + "has_questions_counts = df['has_questions'].value_counts()\n", + "\n", + "# Plot the counts\n", + "plt.figure(figsize=(8, 6))\n", + "has_questions_counts.sort_values().plot(kind='barh')\n", + "plt.ylabel('has_questions')\n", + "plt.xlabel('Count')\n", + "plt.title('Counts Questions vs NO_Questions')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "From the job recommendations point of view the salary and location can be really important parameters to take into consideration. In given dataset we have salary ranges available so there is no need for additional data processing rather than removal of empty ranges but if the dataset you're working on has specific values, consider organizing it into appropriate ranges and only then displaying the result." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 564 + }, + "id": "6SQO5PVLy8vt", + "outputId": "f0dbdf21-af94-4e56-cd82-938b7258c26f" + }, + "outputs": [], + "source": [ + "# Splitting benefits by comma and creating a list of benefits\n", + "benefits_list = df['salary_range'].str.split(',').explode()\n", + "benefits_list = benefits_list[benefits_list != 'None']\n", + "benefits_list = benefits_list[benefits_list != '0-0']\n", + "\n", + "\n", + "# Counting the occurrences of each skill\n", + "benefits_count = benefits_list.str.strip().value_counts()\n", + "\n", + "# Plotting the top 10 most common benefits\n", + "top_benefits = benefits_count.head(10)\n", + "plt.figure(figsize=(10, 6))\n", + "top_benefits.sort_values().plot(kind='barh')\n", + "plt.title('Top 10 Salaries Range Offered by Companies')\n", + "plt.xlabel('Frequency')\n", + "plt.ylabel('Salary Range')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For the location we have both county, state and city specified, so we need to split it into individual columns, and then show top 10 counties and cities." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_242StA_UZTF" + }, + "outputs": [], + "source": [ + "# Split the 'location' column into separate columns for country, state, and city\n", + "location_split = df['location'].str.split(', ', expand=True)\n", + "df['Country'] = location_split[0]\n", + "df['State'] = location_split[1]\n", + "df['City'] = location_split[2]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 959 + }, + "id": "HS9SH6p9UaJU", + "outputId": "6562e31f-6719-448b-c290-1a9610eb50c2" + }, + "outputs": [], + "source": [ + "# Count the occurrences of unique values in the 'Country' column\n", + "Country_counts = df['Country'].value_counts()\n", + "\n", + "# Select the top 10 most frequent occurrences\n", + "top_10_Country = Country_counts.head(10)\n", + "\n", + "# Plot the top 10 most frequent occurrences as horizontal bar plot with rotated labels\n", + "plt.figure(figsize=(14, 10))\n", + "sns.barplot(y=top_10_Country.index, x=top_10_Country.values)\n", + "plt.ylabel('Country')\n", + "plt.xlabel('Count')\n", + "plt.title('Top 10 Most Frequent Country')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 959 + }, + "id": "j_cPJl8pUcWT", + "outputId": "bb87ec2d-750d-45b0-f64f-ae4b84b00544" + }, + "outputs": [], + "source": [ + "# Count the occurrences of unique values in the 'City' column\n", + "City_counts = df['City'].value_counts()\n", + "\n", + "# Select the top 10 most frequent occurrences\n", + "top_10_City = City_counts.head(10)\n", + "\n", + "# Plot the top 10 most frequent occurrences as horizontal bar plot with rotated labels\n", + "plt.figure(figsize=(14, 10))\n", + "sns.barplot(y=top_10_City.index, x=top_10_City.values)\n", + "plt.ylabel('City')\n", + "plt.xlabel('Count')\n", + "plt.title('Top 10 Most Frequent City')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-R8hkAIjVF_s" + }, + "source": [ + "### Fake job postings data visualization \n", + "\n", + "What about fraudulent class? Let see how many of the jobs in the dataset are fake. Whether there are equally true and false offers, or whether there is a significant disproportion between the two. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 651 + }, + "id": "KJ5Aq2IizZ4r", + "outputId": "e1c10006-9f5a-4321-d90a-28915e02f8c3" + }, + "outputs": [], + "source": [ + "## fake job visualization\n", + "# Count the occurrences of unique values in the 'fraudulent' column\n", + "fraudulent_counts = df['fraudulent'].value_counts()\n", + "\n", + "# Plot the counts using a rainbow color palette\n", + "plt.figure(figsize=(8, 6))\n", + "sns.barplot(x=fraudulent_counts.index, y=fraudulent_counts.values)\n", + "plt.xlabel('Fraudulent')\n", + "plt.ylabel('Count')\n", + "plt.title('Counts of Fraudulent vs Non-Fraudulent')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 564 + }, + "id": "oyeB2MFRVIWi", + "outputId": "9236f907-4c16-49f7-c14b-883d21ae6c2c" + }, + "outputs": [], + "source": [ + "plt.figure(figsize=(10, 6))\n", + "sns.countplot(data=df, x='employment_type', hue='fraudulent')\n", + "plt.title('Count of Fraudulent Cases by Employment Type')\n", + "plt.xlabel('Employment Type')\n", + "plt.ylabel('Count')\n", + "plt.legend(title='Fraudulent')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 564 + }, + "id": "ORGFxjVVVJBi", + "outputId": "084304de-5618-436a-8958-6f36abd72be7" + }, + "outputs": [], + "source": [ + "plt.figure(figsize=(10, 6))\n", + "sns.countplot(data=df, x='required_experience', hue='fraudulent')\n", + "plt.title('Count of Fraudulent Cases by Required Experience')\n", + "plt.xlabel('Required Experience')\n", + "plt.ylabel('Count')\n", + "plt.legend(title='Fraudulent')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "id": "GnRPXpBWVL7O", + "outputId": "8d347181-83d8-44ae-9c57-88d98825694d" + }, + "outputs": [], + "source": [ + "plt.figure(figsize=(30, 18))\n", + "sns.countplot(data=df, x='required_education', hue='fraudulent')\n", + "plt.title('Count of Fraudulent Cases by Required Education')\n", + "plt.xlabel('Required Education')\n", + "plt.ylabel('Count')\n", + "plt.legend(title='Fraudulent')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8qKuYrkvVPlO" + }, + "source": [ + "We can see that there is no connection between those parameters and fake job postings. This way in the future processing we can remove them." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BbOwzXmdaJTw" + }, + "source": [ + "## Data cleaning/pre-processing\n", + "\n", + "One of the really important step related to any type of data processing is data cleaning. For texts it usually includes removal of stop words, special characters, numbers or any additional noise like hyperlinks. \n", + "\n", + "In our case, to prepare data for Fake Job Postings recognition we will first, combine all relevant columns into single new record and then clean the data to work on it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "jYLwp2wSaMdi" + }, + "outputs": [], + "source": [ + "# List of columns to concatenate\n", + "columns_to_concat = ['title', 'location', 'department', 'salary_range', 'company_profile',\n", + " 'description', 'requirements', 'benefits', 'employment_type',\n", + " 'required_experience', 'required_education', 'industry', 'function']\n", + "\n", + "# Concatenate the values of specified columns into a new column 'job_posting'\n", + "df['job_posting'] = df[columns_to_concat].apply(lambda x: ' '.join(x.dropna().astype(str)), axis=1)\n", + "\n", + "# Create a new DataFrame with columns 'job_posting' and 'fraudulent'\n", + "new_df = df[['job_posting', 'fraudulent']].copy()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 206 + }, + "id": "FulR3zMiaMgI", + "outputId": "995058f3-f5f7-4aec-e1e0-94d42aad468f" + }, + "outputs": [], + "source": [ + "new_df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "0TpoEx1-YgCs", + "outputId": "8eaaf021-ae66-477a-f07b-fd8a353d17eb", + "scrolled": true + }, + "outputs": [], + "source": [ + "# import spacy\n", + "import re\n", + "import nltk\n", + "from nltk.corpus import stopwords\n", + "\n", + "nltk.download('stopwords')\n", + "\n", + "def preprocess_text(text):\n", + " # Remove newlines, carriage returns, and tabs\n", + " text = re.sub('\\n','', text)\n", + " text = re.sub('\\r','', text)\n", + " text = re.sub('\\t','', text)\n", + " # Remove URLs\n", + " text = re.sub(r\"http\\S+|www\\S+|https\\S+\", \"\", text, flags=re.MULTILINE)\n", + "\n", + " # Remove special characters\n", + " text = re.sub(r\"[^a-zA-Z0-9\\s]\", \"\", text)\n", + "\n", + " # Remove punctuation\n", + " text = re.sub(r'[^\\w\\s]', '', text)\n", + "\n", + " # Remove digits\n", + " text = re.sub(r'\\d', '', text)\n", + "\n", + " # Convert to lowercase\n", + " text = text.lower()\n", + "\n", + " # Remove stop words\n", + " stop_words = set(stopwords.words('english'))\n", + " words = [word for word in text.split() if word.lower() not in stop_words]\n", + " text = ' '.join(words)\n", + "\n", + " return text\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "p9NHS6Vx2BE8" + }, + "outputs": [], + "source": [ + "new_df['job_posting'] = new_df['job_posting'].apply(preprocess_text)\n", + "\n", + "new_df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The next step in the pre-processing is lemmatization. It is a process to reduce a word to its root form, called a lemma. For example the verb 'planning' would be changed to 'plan' world." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "kZnHODi-ZK33" + }, + "outputs": [], + "source": [ + "# Lemmatization\n", + "import en_core_web_sm\n", + "\n", + "nlp = en_core_web_sm.load()\n", + "\n", + "def lemmatize_text(text):\n", + " doc = nlp(text)\n", + " return \" \".join([token.lemma_ for token in doc])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "uQauVQdw2LWa" + }, + "outputs": [], + "source": [ + "new_df['job_posting'] = new_df['job_posting'].apply(lemmatize_text)\n", + "\n", + "new_df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dQDR6_SpZW0B" + }, + "source": [ + "At this stage we can also visualize the data with wordcloud by having special text column. We can show it for both fake and real dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 411 + }, + "id": "fdR9GAG6ZnPh", + "outputId": "57e9b5ae-87b4-4523-d0ae-8c8fd56cd9bc" + }, + "outputs": [], + "source": [ + "from wordcloud import WordCloud\n", + "\n", + "non_fraudulent_text = ' '.join(text for text in new_df[new_df['fraudulent'] == 0]['job_posting'])\n", + "fraudulent_text = ' '.join(text for text in new_df[new_df['fraudulent'] == 1]['job_posting'])\n", + "\n", + "wordcloud_non_fraudulent = WordCloud(width=800, height=400, background_color='white').generate(non_fraudulent_text)\n", + "\n", + "wordcloud_fraudulent = WordCloud(width=800, height=400, background_color='white').generate(fraudulent_text)\n", + "\n", + "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 10))\n", + "\n", + "ax1.imshow(wordcloud_non_fraudulent, interpolation='bilinear')\n", + "ax1.axis('off')\n", + "ax1.set_title('Non-Fraudulent Job Postings')\n", + "\n", + "ax2.imshow(wordcloud_fraudulent, interpolation='bilinear')\n", + "ax2.axis('off')\n", + "ax2.set_title('Fraudulent Job Postings')\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ihtfhOr7aNMa" + }, + "source": [ + "## Fake job postings identification and removal\n", + "\n", + "Nowadays, it is unfortunate that not all the job offers that are posted on papular portals are genuine. Some of them are created only to collect personal data. Therefore, just detecting fake job postings can be very essential. \n", + "\n", + "We will create bidirectional LSTM model with one hot encoding. Let's start with all necessary imports." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "VNdX-xcjtVS2" + }, + "outputs": [], + "source": [ + "from tensorflow.keras.layers import Embedding\n", + "from tensorflow.keras.preprocessing.sequence import pad_sequences\n", + "from tensorflow.keras.models import Sequential\n", + "from tensorflow.keras.preprocessing.text import one_hot\n", + "from tensorflow.keras.layers import Dense\n", + "from tensorflow.keras.layers import Bidirectional\n", + "from tensorflow.keras.layers import Dropout" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Make sure, you're using Tensorflow version 2.15.0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 35 + }, + "id": "IxY47-s7tbjU", + "outputId": "02d68552-ff52-422b-9044-e55e35ef1236" + }, + "outputs": [], + "source": [ + "import tensorflow as tf\n", + "tf.__version__" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let us import Intel Extension for TensorFlow*. We are using Python API `itex.experimental_ops_override()`. It automatically replace some TensorFlow operators by Custom Operators under `itex.ops` namespace, as well as to be compatible with existing trained parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import intel_extension_for_tensorflow as itex\n", + "\n", + "itex.experimental_ops_override()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We need to prepare data for the model we will create. First let's assign job_postings to X and fraudulent values to y (expected value)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "U-7klPFFtZgo" + }, + "outputs": [], + "source": [ + "X = new_df['job_posting']\n", + "y = new_df['fraudulent']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "One hot encoding is a technique to represent categorical variables as numerical values. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "3FFtUrPbtbmD" + }, + "outputs": [], + "source": [ + "voc_size = 5000\n", + "onehot_repr = [one_hot(words, voc_size) for words in X]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "ygHx6LSg6ZUr", + "outputId": "5b152a4f-621b-400c-a65b-5fa19a934aa2" + }, + "outputs": [], + "source": [ + "sent_length = 40\n", + "embedded_docs = pad_sequences(onehot_repr, padding='pre', maxlen=sent_length)\n", + "print(embedded_docs)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Creating model\n", + "\n", + "We are creating Deep Neural Network using Bidirectional LSTM. The architecture is as followed:\n", + "\n", + "* Embedding layer\n", + "* Bidirectiona LSTM Layer\n", + "* Dropout layer\n", + "* Dense layer with sigmod function\n", + "\n", + "We are using Adam optimizer with binary crossentropy. We are optimism accuracy.\n", + "\n", + "If Intel® Extension for TensorFlow* backend is XPU, `tf.keras.layers.LSTM` will be replaced by `itex.ops.ItexLSTM`. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "Vnhm4huG-Mat", + "outputId": "dbc59ef1-168a-4e11-f38d-b47674dd4be6" + }, + "outputs": [], + "source": [ + "embedding_vector_features = 50\n", + "model_itex = Sequential()\n", + "model_itex.add(Embedding(voc_size, embedding_vector_features, input_length=sent_length))\n", + "model_itex.add(Bidirectional(itex.ops.ItexLSTM(100)))\n", + "model_itex.add(Dropout(0.3))\n", + "model_itex.add(Dense(1, activation='sigmoid'))\n", + "model_itex.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])\n", + "print(model_itex.summary())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1-tz3hyc-PvN" + }, + "outputs": [], + "source": [ + "X_final = np.array(embedded_docs)\n", + "y_final = np.array(y)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "POVN7X60-TnQ" + }, + "outputs": [], + "source": [ + "from sklearn.model_selection import train_test_split\n", + "X_train, X_test, y_train, y_test = train_test_split(X_final, y_final, test_size=0.25, random_state=320)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's train the model. We are using standard `model.fit()` method providing training and testing dataset. You can easily modify number of epochs in this training process but keep in mind that the model can become overtrained, so that it will have very good results on training data, but poor results on test data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "U0cGa7ei-Ufh", + "outputId": "68ce942d-ea51-458f-ac6c-ab619ab1ce74" + }, + "outputs": [], + "source": [ + "model_itex.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=1, batch_size=64)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The values returned by the model are in the range [0,1] Need to map them to integer values of 0 or 1." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "u4I8Y-R5EcDw", + "outputId": "be384d88-b27c-49c5-bebb-e9bdba986692" + }, + "outputs": [], + "source": [ + "y_pred = (model_itex.predict(X_test) > 0.5).astype(\"int32\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To demonstrate the effectiveness of our models we presented the confusion matrix and classification report available within the `scikit-learn` library." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 675 + }, + "id": "0lB3N6fxtbom", + "outputId": "97b1713d-b373-44e1-a5b2-15e41aa84016" + }, + "outputs": [], + "source": [ + "from sklearn.metrics import ConfusionMatrixDisplay, confusion_matrix, classification_report\n", + "\n", + "conf_matrix = confusion_matrix(y_test, y_pred)\n", + "print(\"Confusion matrix:\")\n", + "print(conf_matrix)\n", + "\n", + "ConfusionMatrixDisplay.from_predictions(y_test, y_pred)\n", + "\n", + "class_report = classification_report(y_test, y_pred)\n", + "print(\"Classification report:\")\n", + "print(class_report)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ioa6oZNuaPnJ" + }, + "source": [ + "## Job recommendation by showing the most similar ones" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZGReO9ziJyXm" + }, + "source": [ + "Now, as we are sure that the data we are processing is real, we can get back to the original columns and create our recommendation system.\n", + "\n", + "Also use much more simple solution for recommendations. Even, as before we used Deep Learning to check if posting is fake, we can use classical machine learning algorithms to show similar job postings.\n", + "\n", + "First, let's filter fake job postings." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 556 + }, + "id": "RsCZLWU0aMqN", + "outputId": "503c1b4e-26db-46fd-d69f-f8ee17c1519c" + }, + "outputs": [], + "source": [ + "real = df[df['fraudulent'] == 0]\n", + "real.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After that, we create a common column containing those text parameters that we want to be compared between theses and are relevant to us when making recommendations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 206 + }, + "id": "NLc-uoYeaMsy", + "outputId": "452602b0-88e2-4c9c-a6f0-5b069cc34009" + }, + "outputs": [], + "source": [ + "cols = ['title', 'description', 'requirements', 'required_experience', 'required_education', 'industry']\n", + "real = real[cols]\n", + "real.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 293 + }, + "id": "mX-xc2OetVzx", + "outputId": "e0f24240-d8dd-4f79-fda6-db15d2f4c54f" + }, + "outputs": [], + "source": [ + "real = real.fillna(value='')\n", + "real['text'] = real['description'] + real['requirements'] + real['required_experience'] + real['required_education'] + real['industry']\n", + "real.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's see the mechanism that we will use to prepare recommendations - we will use sentence similarity based on prepared `text` column in our dataset. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from sentence_transformers import SentenceTransformer\n", + "\n", + "model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's prepare a few example sentences that cover 4 topics. On these sentences it will be easier to show how the similarities between the texts work than on the whole large dataset we have." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "messages = [\n", + " # Smartphones\n", + " \"I like my phone\",\n", + " \"My phone is not good.\",\n", + " \"Your cellphone looks great.\",\n", + "\n", + " # Weather\n", + " \"Will it snow tomorrow?\",\n", + " \"Recently a lot of hurricanes have hit the US\",\n", + " \"Global warming is real\",\n", + "\n", + " # Food and health\n", + " \"An apple a day, keeps the doctors away\",\n", + " \"Eating strawberries is healthy\",\n", + " \"Is paleo better than keto?\",\n", + "\n", + " # Asking about age\n", + " \"How old are you?\",\n", + " \"what is your age?\",\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we are preparing functions to show similarities between given sentences in the for of heat map. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import seaborn as sns\n", + "\n", + "def plot_similarity(labels, features, rotation):\n", + " corr = np.inner(features, features)\n", + " sns.set(font_scale=1.2)\n", + " g = sns.heatmap(\n", + " corr,\n", + " xticklabels=labels,\n", + " yticklabels=labels,\n", + " vmin=0,\n", + " vmax=1,\n", + " cmap=\"YlOrRd\")\n", + " g.set_xticklabels(labels, rotation=rotation)\n", + " g.set_title(\"Semantic Textual Similarity\")\n", + "\n", + "def run_and_plot(messages_):\n", + " message_embeddings_ = model.encode(messages_)\n", + " plot_similarity(messages_, message_embeddings_, 90)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "run_and_plot(messages)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's move back to our job postings dataset. First, we are using sentence encoding model to be able to calculate similarities." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "encodings = []\n", + "for text in real['text']:\n", + " encodings.append(model.encode(text))\n", + "\n", + "real['encodings'] = encodings" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then, we can chose job posting we wan to calculate similarities to. In our case it is first job posting in the dataset, but you can easily change it to any other job posting, by changing value in the `index` variable." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "index = 0\n", + "corr = np.inner(encodings[index], encodings)\n", + "real['corr_to_first'] = corr" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And based on the calculated similarities, we can show top most similar job postings, by sorting them according to calculated correlation value." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "real.sort_values(by=['corr_to_first'], ascending=False).head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this code sample we created job recommendation system. First, we explored and analyzed the dataset, then we pre-process the data and create fake job postings detection model. At the end we used sentence similarities to show top 5 recommendations - the most similar job descriptions to the chosen one. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"[CODE_SAMPLE_COMPLETED_SUCCESSFULLY]\")" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Tensorflow", + "language": "python", + "name": "tensorflow" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/JobRecommendationSystem.py b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/JobRecommendationSystem.py new file mode 100644 index 0000000000..425ab1f5dd --- /dev/null +++ b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/JobRecommendationSystem.py @@ -0,0 +1,634 @@ +# %% [markdown] +# # Job recommendation system +# +# The code sample contains the following parts: +# +# 1. Data exploration and visualization +# 2. Data cleaning/pre-processing +# 3. Fake job postings identification and removal +# 4. Job recommendation by showing the most similar job postings +# +# The scenario is that someone wants to find the best posting for themselves. They have collected the data, but he is not sure if all the data is real. Therefore, based on a trained model, as in this sample, they identify with a high degree of accuracy which postings are real, and it is among them that they choose the best ad for themselves. +# +# For simplicity, only one dataset will be used within this code, but the process using one dataset is not significantly different from the one described earlier. +# + +# %% [markdown] +# ## Data exploration and visualization +# +# For the purpose of this code sample we will use Real or Fake: Fake Job Postings dataset available over HuggingFace API. In this first part we will focus on data exploration and visualization. In standard end-to-end workload it is the first step. Engineer needs to first know the data to be able to work on it and prepare solution that will utilize dataset the best. +# +# Lest start with loading the dataset. We are using datasets library to do that. + +# %% +from datasets import load_dataset + +dataset = load_dataset("victor/real-or-fake-fake-jobposting-prediction") +dataset = dataset['train'] + +# %% [markdown] +# To better analyze and understand the data we are transferring it to pandas DataFrame, so we are able to take benefit from all pandas data transformations. Pandas library provides multiple useful functions for data manipulation so it is usual choice at this stage of machine learning or deep learning project. +# + +# %% +import pandas as pd +df = dataset.to_pandas() + +# %% [markdown] +# Let's see 5 first and 5 last rows in the dataset we are working on. + +# %% +df.head() + +# %% +df.tail() + +# %% [markdown] +# Now, lets print a concise summary of the dataset. This way we will see all the column names, know the number of rows and types in every of the column. It is a great overview on the features of the dataset. + +# %% +df.info() + +# %% [markdown] +# At this point it is a good idea to make sure our dataset doen't contain any data duplication that could impact the results of our future system. To do that we firs need to remove `job_id` column. It contains unique number for each job posting so even if the rest of the data is the same between 2 postings it makes it different. + +# %% +# Drop the 'job_id' column +df = df.drop(columns=['job_id']) +df.head() + +# %% [markdown] +# And now, the actual duplicates removal. We first pring the number of duplicates that are in our dataset, than using `drop_duplicated` method we are removing them and after this operation printing the number of the duplicates. If everything works as expected after duplicates removal we should print `0` as current number of duplicates in the dataset. + +# %% +# let's make sure that there are no duplicated jobs + +print(df.duplicated().sum()) +df = df.drop_duplicates() +print(df.duplicated().sum()) + +# %% [markdown] +# Now we can visualize the data from the dataset. First let's visualize data as it is all real, and later, for the purposes of the fake data detection, we will also visualize it spreading fake and real data. +# +# When working with text data it can be challenging to visualize it. Thankfully, there is a `wordcloud` library that shows common words in the analyzed texts. The bigger word is, more often the word is in the text. Wordclouds allow us to quickly identify the most important topic and themes in a large text dataset and also explore patterns and trends in textural data. +# +# In our example, we will create wordcloud for job titles, to have high-level overview of job postings we are working with. + +# %% +from wordcloud import WordCloud # module to print word cloud +from matplotlib import pyplot as plt +import seaborn as sns + +# On the basis of Job Titles form word cloud +job_titles_text = ' '.join(df['title']) +wordcloud = WordCloud(width=800, height=400, background_color='white').generate(job_titles_text) + +# Plotting Word Cloud +plt.figure(figsize=(10, 6)) +plt.imshow(wordcloud, interpolation='bilinear') +plt.title('Job Titles') +plt.axis('off') +plt.tight_layout() +plt.show() + +# %% [markdown] +# Different possibility to get some information from this type of dataset is by showing top-n most common values in given column or distribution of the values int his column. +# Let's show top 10 most common job titles and compare this result with previously showed wordcould. + +# %% +# Get Count of job title +job_title_counts = df['title'].value_counts() + +# Plotting a bar chart for the top 10 most common job titles +top_job_titles = job_title_counts.head(10) +plt.figure(figsize=(10, 6)) +top_job_titles.sort_values().plot(kind='barh') +plt.title('Top 10 Most Common Job Titles') +plt.xlabel('Frequency') +plt.ylabel('Job Titles') +plt.show() + +# %% [markdown] +# Now we can do the same for different columns, as `employment_type`, `required_experience`, `telecommuting`, `has_company_logo` and `has_questions`. These should give us reale good overview of different parts of our dataset. + +# %% +# Count the occurrences of each work type +work_type_counts = df['employment_type'].value_counts() + +# Plotting the distribution of work types +plt.figure(figsize=(8, 6)) +work_type_counts.sort_values().plot(kind='barh') +plt.title('Distribution of Work Types Offered by Jobs') +plt.xlabel('Frequency') +plt.ylabel('Work Types') +plt.show() + +# %% +# Count the occurrences of required experience types +work_type_counts = df['required_experience'].value_counts() + +# Plotting the distribution of work types +plt.figure(figsize=(8, 6)) +work_type_counts.sort_values().plot(kind='barh') +plt.title('Distribution of Required Experience by Jobs') +plt.xlabel('Frequency') +plt.ylabel('Required Experience') +plt.show() + +# %% [markdown] +# For employment_type and required_experience we also created matrix to see if there is any corelation between those two. To visualize it we created heatmap. If you think that some of the parameters can be related, creating similar heatmap can be a good idea. + +# %% +from matplotlib import pyplot as plt +import seaborn as sns +import pandas as pd + +plt.subplots(figsize=(8, 8)) +df_2dhist = pd.DataFrame({ + x_label: grp['required_experience'].value_counts() + for x_label, grp in df.groupby('employment_type') +}) +sns.heatmap(df_2dhist, cmap='viridis') +plt.xlabel('employment_type') +_ = plt.ylabel('required_experience') + +# %% +# Count the occurrences of unique values in the 'telecommuting' column +telecommuting_counts = df['telecommuting'].value_counts() + +plt.figure(figsize=(8, 6)) +telecommuting_counts.sort_values().plot(kind='barh') +plt.title('Counts of telecommuting vs Non-telecommuting') +plt.xlabel('count') +plt.ylabel('telecommuting') +plt.show() + +# %% +has_company_logo_counts = df['has_company_logo'].value_counts() + +plt.figure(figsize=(8, 6)) +has_company_logo_counts.sort_values().plot(kind='barh') +plt.ylabel('has_company_logo') +plt.xlabel('Count') +plt.title('Counts of With_Logo vs Without_Logo') +plt.show() + +# %% +has_questions_counts = df['has_questions'].value_counts() + +# Plot the counts +plt.figure(figsize=(8, 6)) +has_questions_counts.sort_values().plot(kind='barh') +plt.ylabel('has_questions') +plt.xlabel('Count') +plt.title('Counts Questions vs NO_Questions') +plt.show() + +# %% [markdown] +# From the job recommendations point of view the salary and location can be really important parameters to take into consideration. In given dataset we have salary ranges available so there is no need for additional data processing rather than removal of empty ranges but if the dataset you're working on has specific values, consider organizing it into appropriate ranges and only then displaying the result. + +# %% +# Splitting benefits by comma and creating a list of benefits +benefits_list = df['salary_range'].str.split(',').explode() +benefits_list = benefits_list[benefits_list != 'None'] +benefits_list = benefits_list[benefits_list != '0-0'] + + +# Counting the occurrences of each skill +benefits_count = benefits_list.str.strip().value_counts() + +# Plotting the top 10 most common benefits +top_benefits = benefits_count.head(10) +plt.figure(figsize=(10, 6)) +top_benefits.sort_values().plot(kind='barh') +plt.title('Top 10 Salaries Range Offered by Companies') +plt.xlabel('Frequency') +plt.ylabel('Salary Range') +plt.show() + +# %% [markdown] +# For the location we have both county, state and city specified, so we need to split it into individual columns, and then show top 10 counties and cities. + +# %% +# Split the 'location' column into separate columns for country, state, and city +location_split = df['location'].str.split(', ', expand=True) +df['Country'] = location_split[0] +df['State'] = location_split[1] +df['City'] = location_split[2] + +# %% +# Count the occurrences of unique values in the 'Country' column +Country_counts = df['Country'].value_counts() + +# Select the top 10 most frequent occurrences +top_10_Country = Country_counts.head(10) + +# Plot the top 10 most frequent occurrences as horizontal bar plot with rotated labels +plt.figure(figsize=(14, 10)) +sns.barplot(y=top_10_Country.index, x=top_10_Country.values) +plt.ylabel('Country') +plt.xlabel('Count') +plt.title('Top 10 Most Frequent Country') +plt.show() + +# %% +# Count the occurrences of unique values in the 'City' column +City_counts = df['City'].value_counts() + +# Select the top 10 most frequent occurrences +top_10_City = City_counts.head(10) + +# Plot the top 10 most frequent occurrences as horizontal bar plot with rotated labels +plt.figure(figsize=(14, 10)) +sns.barplot(y=top_10_City.index, x=top_10_City.values) +plt.ylabel('City') +plt.xlabel('Count') +plt.title('Top 10 Most Frequent City') +plt.show() + +# %% [markdown] +# ### Fake job postings data visualization +# +# What about fraudulent class? Let see how many of the jobs in the dataset are fake. Whether there are equally true and false offers, or whether there is a significant disproportion between the two. + +# %% +## fake job visualization +# Count the occurrences of unique values in the 'fraudulent' column +fraudulent_counts = df['fraudulent'].value_counts() + +# Plot the counts using a rainbow color palette +plt.figure(figsize=(8, 6)) +sns.barplot(x=fraudulent_counts.index, y=fraudulent_counts.values) +plt.xlabel('Fraudulent') +plt.ylabel('Count') +plt.title('Counts of Fraudulent vs Non-Fraudulent') +plt.show() + +# %% +plt.figure(figsize=(10, 6)) +sns.countplot(data=df, x='employment_type', hue='fraudulent') +plt.title('Count of Fraudulent Cases by Employment Type') +plt.xlabel('Employment Type') +plt.ylabel('Count') +plt.legend(title='Fraudulent') +plt.show() + +# %% +plt.figure(figsize=(10, 6)) +sns.countplot(data=df, x='required_experience', hue='fraudulent') +plt.title('Count of Fraudulent Cases by Required Experience') +plt.xlabel('Required Experience') +plt.ylabel('Count') +plt.legend(title='Fraudulent') +plt.show() + +# %% +plt.figure(figsize=(30, 18)) +sns.countplot(data=df, x='required_education', hue='fraudulent') +plt.title('Count of Fraudulent Cases by Required Education') +plt.xlabel('Required Education') +plt.ylabel('Count') +plt.legend(title='Fraudulent') +plt.show() + +# %% [markdown] +# We can see that there is no connection between those parameters and fake job postings. This way in the future processing we can remove them. + +# %% [markdown] +# ## Data cleaning/pre-processing +# +# One of the really important step related to any type of data processing is data cleaning. For texts it usually includes removal of stop words, special characters, numbers or any additional noise like hyperlinks. +# +# In our case, to prepare data for Fake Job Postings recognition we will first, combine all relevant columns into single new record and then clean the data to work on it. + +# %% +# List of columns to concatenate +columns_to_concat = ['title', 'location', 'department', 'salary_range', 'company_profile', + 'description', 'requirements', 'benefits', 'employment_type', + 'required_experience', 'required_education', 'industry', 'function'] + +# Concatenate the values of specified columns into a new column 'job_posting' +df['job_posting'] = df[columns_to_concat].apply(lambda x: ' '.join(x.dropna().astype(str)), axis=1) + +# Create a new DataFrame with columns 'job_posting' and 'fraudulent' +new_df = df[['job_posting', 'fraudulent']].copy() + +# %% +new_df.head() + +# %% +# import spacy +import re +import nltk +from nltk.corpus import stopwords + +nltk.download('stopwords') + +def preprocess_text(text): + # Remove newlines, carriage returns, and tabs + text = re.sub('\n','', text) + text = re.sub('\r','', text) + text = re.sub('\t','', text) + # Remove URLs + text = re.sub(r"http\S+|www\S+|https\S+", "", text, flags=re.MULTILINE) + + # Remove special characters + text = re.sub(r"[^a-zA-Z0-9\s]", "", text) + + # Remove punctuation + text = re.sub(r'[^\w\s]', '', text) + + # Remove digits + text = re.sub(r'\d', '', text) + + # Convert to lowercase + text = text.lower() + + # Remove stop words + stop_words = set(stopwords.words('english')) + words = [word for word in text.split() if word.lower() not in stop_words] + text = ' '.join(words) + + return text + + + +# %% +new_df['job_posting'] = new_df['job_posting'].apply(preprocess_text) + +new_df.head() + +# %% [markdown] +# The next step in the pre-processing is lemmatization. It is a process to reduce a word to its root form, called a lemma. For example the verb 'planning' would be changed to 'plan' world. + +# %% +# Lemmatization +import en_core_web_sm + +nlp = en_core_web_sm.load() + +def lemmatize_text(text): + doc = nlp(text) + return " ".join([token.lemma_ for token in doc]) + +# %% +new_df['job_posting'] = new_df['job_posting'].apply(lemmatize_text) + +new_df.head() + +# %% [markdown] +# At this stage we can also visualize the data with wordcloud by having special text column. We can show it for both fake and real dataset. + +# %% +from wordcloud import WordCloud + +non_fraudulent_text = ' '.join(text for text in new_df[new_df['fraudulent'] == 0]['job_posting']) +fraudulent_text = ' '.join(text for text in new_df[new_df['fraudulent'] == 1]['job_posting']) + +wordcloud_non_fraudulent = WordCloud(width=800, height=400, background_color='white').generate(non_fraudulent_text) + +wordcloud_fraudulent = WordCloud(width=800, height=400, background_color='white').generate(fraudulent_text) + +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 10)) + +ax1.imshow(wordcloud_non_fraudulent, interpolation='bilinear') +ax1.axis('off') +ax1.set_title('Non-Fraudulent Job Postings') + +ax2.imshow(wordcloud_fraudulent, interpolation='bilinear') +ax2.axis('off') +ax2.set_title('Fraudulent Job Postings') + +plt.show() + +# %% [markdown] +# ## Fake job postings identification and removal +# +# Nowadays, it is unfortunate that not all the job offers that are posted on papular portals are genuine. Some of them are created only to collect personal data. Therefore, just detecting fake job postings can be very essential. +# +# We will create bidirectional LSTM model with one hot encoding. Let's start with all necessary imports. + +# %% +from tensorflow.keras.layers import Embedding +from tensorflow.keras.preprocessing.sequence import pad_sequences +from tensorflow.keras.models import Sequential +from tensorflow.keras.preprocessing.text import one_hot +from tensorflow.keras.layers import Dense +from tensorflow.keras.layers import Bidirectional +from tensorflow.keras.layers import Dropout + +# %% [markdown] +# Make sure, you're using Tensorflow version 2.15.0 + +# %% +import tensorflow as tf +tf.__version__ + +# %% [markdown] +# Now, let us import Intel Extension for TensorFlow*. We are using Python API `itex.experimental_ops_override()`. It automatically replace some TensorFlow operators by Custom Operators under `itex.ops` namespace, as well as to be compatible with existing trained parameters. + +# %% +import intel_extension_for_tensorflow as itex + +itex.experimental_ops_override() + +# %% [markdown] +# We need to prepare data for the model we will create. First let's assign job_postings to X and fraudulent values to y (expected value). + +# %% +X = new_df['job_posting'] +y = new_df['fraudulent'] + +# %% [markdown] +# One hot encoding is a technique to represent categorical variables as numerical values. + +# %% +voc_size = 5000 +onehot_repr = [one_hot(words, voc_size) for words in X] + +# %% +sent_length = 40 +embedded_docs = pad_sequences(onehot_repr, padding='pre', maxlen=sent_length) +print(embedded_docs) + +# %% [markdown] +# ### Creating model +# +# We are creating Deep Neural Network using Bidirectional LSTM. The architecture is as followed: +# +# * Embedding layer +# * Bidirectiona LSTM Layer +# * Dropout layer +# * Dense layer with sigmod function +# +# We are using Adam optimizer with binary crossentropy. We are optimism accuracy. +# +# If Intel® Extension for TensorFlow* backend is XPU, `tf.keras.layers.LSTM` will be replaced by `itex.ops.ItexLSTM`. + +# %% +embedding_vector_features = 50 +model_itex = Sequential() +model_itex.add(Embedding(voc_size, embedding_vector_features, input_length=sent_length)) +model_itex.add(Bidirectional(itex.ops.ItexLSTM(100))) +model_itex.add(Dropout(0.3)) +model_itex.add(Dense(1, activation='sigmoid')) +model_itex.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) +print(model_itex.summary()) + +# %% +X_final = np.array(embedded_docs) +y_final = np.array(y) + +# %% [markdown] +# + +# %% +from sklearn.model_selection import train_test_split +X_train, X_test, y_train, y_test = train_test_split(X_final, y_final, test_size=0.25, random_state=320) + +# %% [markdown] +# Now, let's train the model. We are using standard `model.fit()` method providing training and testing dataset. You can easily modify number of epochs in this training process but keep in mind that the model can become overtrained, so that it will have very good results on training data, but poor results on test data. + +# %% +model_itex.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=1, batch_size=64) + +# %% [markdown] +# The values returned by the model are in the range [0,1] Need to map them to integer values of 0 or 1. + +# %% +y_pred = (model_itex.predict(X_test) > 0.5).astype("int32") + +# %% [markdown] +# To demonstrate the effectiveness of our models we presented the confusion matrix and classification report available within the `scikit-learn` library. + +# %% +from sklearn.metrics import ConfusionMatrixDisplay, confusion_matrix, classification_report + +conf_matrix = confusion_matrix(y_test, y_pred) +print("Confusion matrix:") +print(conf_matrix) + +ConfusionMatrixDisplay.from_predictions(y_test, y_pred) + +class_report = classification_report(y_test, y_pred) +print("Classification report:") +print(class_report) + +# %% [markdown] +# ## Job recommendation by showing the most similar ones + +# %% [markdown] +# Now, as we are sure that the data we are processing is real, we can get back to the original columns and create our recommendation system. +# +# Also use much more simple solution for recommendations. Even, as before we used Deep Learning to check if posting is fake, we can use classical machine learning algorithms to show similar job postings. +# +# First, let's filter fake job postings. + +# %% +real = df[df['fraudulent'] == 0] +real.head() + +# %% [markdown] +# After that, we create a common column containing those text parameters that we want to be compared between theses and are relevant to us when making recommendations. + +# %% +cols = ['title', 'description', 'requirements', 'required_experience', 'required_education', 'industry'] +real = real[cols] +real.head() + +# %% +real = real.fillna(value='') +real['text'] = real['description'] + real['requirements'] + real['required_experience'] + real['required_education'] + real['industry'] +real.head() + +# %% [markdown] +# Let's see the mechanism that we will use to prepare recommendations - we will use sentence similarity based on prepared `text` column in our dataset. + +# %% +from sentence_transformers import SentenceTransformer + +model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2') + +# %% [markdown] +# Let's prepare a few example sentences that cover 4 topics. On these sentences it will be easier to show how the similarities between the texts work than on the whole large dataset we have. + +# %% +messages = [ + # Smartphones + "I like my phone", + "My phone is not good.", + "Your cellphone looks great.", + + # Weather + "Will it snow tomorrow?", + "Recently a lot of hurricanes have hit the US", + "Global warming is real", + + # Food and health + "An apple a day, keeps the doctors away", + "Eating strawberries is healthy", + "Is paleo better than keto?", + + # Asking about age + "How old are you?", + "what is your age?", +] + +# %% [markdown] +# Now, we are preparing functions to show similarities between given sentences in the for of heat map. + +# %% +import numpy as np +import seaborn as sns + +def plot_similarity(labels, features, rotation): + corr = np.inner(features, features) + sns.set(font_scale=1.2) + g = sns.heatmap( + corr, + xticklabels=labels, + yticklabels=labels, + vmin=0, + vmax=1, + cmap="YlOrRd") + g.set_xticklabels(labels, rotation=rotation) + g.set_title("Semantic Textual Similarity") + +def run_and_plot(messages_): + message_embeddings_ = model.encode(messages_) + plot_similarity(messages_, message_embeddings_, 90) + +# %% +run_and_plot(messages) + +# %% [markdown] +# Now, let's move back to our job postings dataset. First, we are using sentence encoding model to be able to calculate similarities. + +# %% +encodings = [] +for text in real['text']: + encodings.append(model.encode(text)) + +real['encodings'] = encodings + +# %% [markdown] +# Then, we can chose job posting we wan to calculate similarities to. In our case it is first job posting in the dataset, but you can easily change it to any other job posting, by changing value in the `index` variable. + +# %% +index = 0 +corr = np.inner(encodings[index], encodings) +real['corr_to_first'] = corr + +# %% [markdown] +# And based on the calculated similarities, we can show top most similar job postings, by sorting them according to calculated correlation value. + +# %% +real.sort_values(by=['corr_to_first'], ascending=False).head() + +# %% [markdown] +# In this code sample we created job recommendation system. First, we explored and analyzed the dataset, then we pre-process the data and create fake job postings detection model. At the end we used sentence similarities to show top 5 recommendations - the most similar job descriptions to the chosen one. + +# %% +print("[CODE_SAMPLE_COMPLETED_SUCCESSFULLY]") + + diff --git a/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/License.txt b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/License.txt new file mode 100644 index 0000000000..e63c6e13dc --- /dev/null +++ b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/License.txt @@ -0,0 +1,7 @@ +Copyright Intel Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/README.md b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/README.md new file mode 100644 index 0000000000..6964819ee4 --- /dev/null +++ b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/README.md @@ -0,0 +1,177 @@ +# Job Recommendation System: End-to-End Deep Learning Workload + + +This sample illustrates the use of Intel® Extension for TensorFlow* to build and run an end-to-end AI workload on the example of the job recommendation system. + +| Property | Description +|:--- |:--- +| Category | Reference Designs and End to End +| What you will learn | How to use Intel® Extension for TensorFlow* to build end to end AI workload? +| Time to complete | 30 minutes + +## Purpose + +This code sample show end-to-end Deep Learning workload in the example of job recommendation system. It consists of four main parts: + +1. Data exploration and visualization - showing what the dataset is looking like, what are some of the main features and what is a data distribution in it. +2. Data cleaning and pre-processing - removal of duplicates, explanation all necessary steps for text pre-processing. +3. Fraud job postings removal - finding which of the job posting are fake using LSTM DNN and filtering them. +4. Job recommendation - calculation and providing top-n job descriptions similar to the chosen one. + +## Prerequisites + +| Optimized for | Description +| :--- | :--- +| OS | Linux, Ubuntu* 20.04 +| Hardware | GPU +| Software | Intel® Extension for TensorFlow* +> **Note**: AI and Analytics samples are validated on AI Tools Offline Installer. For the full list of validated platforms refer to [Platform Validation](https://github.com/oneapi-src/oneAPI-samples/tree/master?tab=readme-ov-file#platform-validation). + + +## Key Implementation Details + +This sample creates Deep Neural Networ to fake job postings detections using Intel® Extension for TensorFlow* LSTM layer on GPU. It also utilizes `itex.experimental_ops_override()` to automatically replace some TensorFlow operators by Custom Operators form Intel® Extension for TensorFlow*. + +The sample tutorial contains one Jupyter Notebook and one Python script. You can use either. + +## Environment Setup +You will need to download and install the following toolkits, tools, and components to use the sample. + + +**1. Get AI Tools** + +Required AI Tools: + +If you have not already, select and install these Tools via [AI Tools Selector](https://www.intel.com/content/www/us/en/developer/tools/oneapi/ai-tools-selector.html). AI and Analytics samples are validated on AI Tools Offline Installer. It is recommended to select Offline Installer option in AI Tools Selector. + +>**Note**: If Docker option is chosen in AI Tools Selector, refer to [Working with Preset Containers](https://github.com/intel/ai-containers/tree/main/preset) to learn how to run the docker and samples. + +**2. (Offline Installer) Activate the AI Tools bundle base environment** + +If the default path is used during the installation of AI Tools: +``` +source $HOME/intel/oneapi/intelpython/bin/activate +``` +If a non-default path is used: +``` +source /bin/activate +``` + +**3. (Offline Installer) Activate relevant Conda environment** + +``` +conda activate tensorflow-gpu +``` + +**4. Clone the GitHub repository** + + +``` +git clone https://github.com/oneapi-src/oneAPI-samples.git +cd oneAPI-samples/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem +``` + +**5. Install dependencies** + +>**Note**: Before running the following commands, make sure your Conda/Python environment with AI Tools installed is activated + +``` +pip install -r requirements.txt +pip install notebook +``` +For Jupyter Notebook, refer to [Installing Jupyter](https://jupyter.org/install) for detailed installation instructions. + +## Run the Sample +>**Note**: Before running the sample, make sure [Environment Setup](https://github.com/oneapi-src/oneAPI-samples/tree/master/AI-and-Analytics/Getting-Started-Samples/INC-Quantization-Sample-for-PyTorch#environment-setup) is completed. + +Go to the section which corresponds to the installation method chosen in [AI Tools Selector](https://www.intel.com/content/www/us/en/developer/tools/oneapi/ai-tools-selector.html) to see relevant instructions: +* [AI Tools Offline Installer (Validated)](#ai-tools-offline-installer-validated) +* [Conda/PIP](#condapip) +* [Docker](#docker) + +### AI Tools Offline Installer (Validated) + +**1. Register Conda kernel to Jupyter Notebook kernel** + +If the default path is used during the installation of AI Tools: +``` +$HOME/intel/oneapi/intelpython/envs/tensorflow-gpu/bin/python -m ipykernel install --user --name=tensorflow-gpu +``` +If a non-default path is used: +``` +/bin/python -m ipykernel install --user --name=tensorflow-gpu +``` +**2. Launch Jupyter Notebook** + +``` +jupyter notebook --ip=0.0.0.0 +``` +**3. Follow the instructions to open the URL with the token in your browser** + +**4. Select the Notebook** + +``` +JobRecommendationSystem.ipynb +``` +**5. Change the kernel to `tensorflow-gpu`** + +**6. Run every cell in the Notebook in sequence** + +### Conda/PIP +> **Note**: Before running the instructions below, make sure your Conda/Python environment with AI Tools installed is activated + +**1. Register Conda/Python kernel to Jupyter Notebook kernel** + +For Conda: +``` +/bin/python -m ipykernel install --user --name=tensorflow-gpu +``` +To know , run `conda env list` and find your Conda environment path. + +For PIP: +``` +python -m ipykernel install --user --name=tensorflow-gpu +``` +**2. Launch Jupyter Notebook** + +``` +jupyter notebook --ip=0.0.0.0 +``` +**3. Follow the instructions to open the URL with the token in your browser** + +**4. Select the Notebook** + +``` +JobRecommendationSystem.ipynb +``` +**5. Change the kernel to ``** + + +**6. Run every cell in the Notebook in sequence** + +### Docker +AI Tools Docker images already have Get Started samples pre-installed. Refer to [Working with Preset Containers](https://github.com/intel/ai-containers/tree/main/preset) to learn how to run the docker and samples. + + + +## Example Output + + If successful, the sample displays [CODE_SAMPLE_COMPLETED_SUCCESSFULLY]. Additionally, the sample shows multiple diagram explaining dataset, the training progress for fraud job posting detection and top job recommendations. + +## Related Samples + + +* [Intel Extension For TensorFlow Getting Started Sample](https://github.com/oneapi-src/oneAPI-samples/blob/development/AI-and-Analytics/Getting-Started-Samples/Intel_Extension_For_TensorFlow_GettingStarted/README.md) +* [Leveraging Intel Extension for TensorFlow with LSTM for Text Generation Sample](https://github.com/oneapi-src/oneAPI-samples/blob/master/AI-and-Analytics/Features-and-Functionality/IntelTensorFlow_TextGeneration_with_LSTM/README.md) + +## License + +Code samples are licensed under the MIT license. See +[License.txt](https://github.com/oneapi-src/oneAPI-samples/blob/master/License.txt) +for details. + +Third party program Licenses can be found here: +[third-party-programs.txt](https://github.com/oneapi-src/oneAPI-samples/blob/master/third-party-programs.txt) + +*Other names and brands may be claimed as the property of others. [Trademarks](https://www.intel.com/content/www/us/en/legal/trademarks.html) diff --git a/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/requirements.txt b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/requirements.txt new file mode 100644 index 0000000000..15bcd710c6 --- /dev/null +++ b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/requirements.txt @@ -0,0 +1,10 @@ +ipykernel +matplotlib +sentence_transformers +transformers +datasets +accelerate +wordcloud +spacy +jinja2 +nltk \ No newline at end of file diff --git a/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/sample.json b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/sample.json new file mode 100644 index 0000000000..31e14cab36 --- /dev/null +++ b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/sample.json @@ -0,0 +1,29 @@ +{ + "guid": "80708728-0BD4-435E-961D-178E5ED1450C", + "name": "JobRecommendationSystem: End-to-End Deep Learning Workload", + "categories": ["Toolkit/oneAPI AI And Analytics/End-to-End Workloads"], + "description": "This sample illustrates the use of Intel® Extension for TensorFlow* to build and run an end-to-end AI workload on the example of the job recommendation system", + "builder": ["cli"], + "toolchain": ["jupyter"], + "languages": [{"python":{}}], + "os":["linux"], + "targetDevice": ["GPU"], + "ciTests": { + "linux": [ + { + "env": [], + "id": "JobRecommendationSystem_py", + "steps": [ + "source /intel/oneapi/intelpython/bin/activate", + "conda env remove -n user_tensorflow-gpu", + "conda create --name user_tensorflow-gpu --clone tensorflow-gpu", + "conda activate user_tensorflow-gpu", + "pip install -r requirements.txt", + "python -m ipykernel install --user --name=user_tensorflow-gpu", + "python JobRecommendationSystem.py" + ] + } + ] +}, +"expertise": "Reference Designs and End to End" +} \ No newline at end of file diff --git a/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/third-party-programs.txt b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/third-party-programs.txt new file mode 100644 index 0000000000..e9f8042d0a --- /dev/null +++ b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/third-party-programs.txt @@ -0,0 +1,253 @@ +oneAPI Code Samples - Third Party Programs File + +This file contains the list of third party software ("third party programs") +contained in the Intel software and their required notices and/or license +terms. This third party software, even if included with the distribution of the +Intel software, may be governed by separate license terms, including without +limitation, third party license terms, other Intel software license terms, and +open source software license terms. These separate license terms govern your use +of the third party programs as set forth in the “third-party-programs.txt” or +other similarly named text file. + +Third party programs and their corresponding required notices and/or license +terms are listed below. + +-------------------------------------------------------------------------------- + +1. Nothings STB Libraries + +stb/LICENSE + + This software is available under 2 licenses -- choose whichever you prefer. + ------------------------------------------------------------------------------ + ALTERNATIVE A - MIT License + Copyright (c) 2017 Sean Barrett + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is furnished to do + so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + ------------------------------------------------------------------------------ + ALTERNATIVE B - Public Domain (www.unlicense.org) + This is free and unencumbered software released into the public domain. + Anyone is free to copy, modify, publish, use, compile, sell, or distribute this + software, either in source code form or as a compiled binary, for any purpose, + commercial or non-commercial, and by any means. + In jurisdictions that recognize copyright laws, the author or authors of this + software dedicate any and all copyright interest in the software to the public + domain. We make this dedication for the benefit of the public at large and to + the detriment of our heirs and successors. We intend this dedication to be an + overt act of relinquishment in perpetuity of all present and future rights to + this software under copyright law. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +-------------------------------------------------------------------------------- + +2. FGPA example designs-gzip + + SDL2.0 + +zlib License + + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + +-------------------------------------------------------------------------------- + +3. Nbody + (c) 2019 Fabio Baruffa + + Plotly.js + Copyright (c) 2020 Plotly, Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +© 2020 GitHub, Inc. + +-------------------------------------------------------------------------------- + +4. GNU-EFI + Copyright (c) 1998-2000 Intel Corporation + +The files in the "lib" and "inc" subdirectories are using the EFI Application +Toolkit distributed by Intel at http://developer.intel.com/technology/efi + +This code is covered by the following agreement: + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INTEL BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. THE EFI SPECIFICATION AND ALL OTHER INFORMATION +ON THIS WEB SITE ARE PROVIDED "AS IS" WITH NO WARRANTIES, AND ARE SUBJECT +TO CHANGE WITHOUT NOTICE. + +-------------------------------------------------------------------------------- + +5. Edk2 + Copyright (c) 2019, Intel Corporation. All rights reserved. + + Edk2 Basetools + Copyright (c) 2019, Intel Corporation. All rights reserved. + +SPDX-License-Identifier: BSD-2-Clause-Patent + +-------------------------------------------------------------------------------- + +6. Heat Transmission + +GNU LESSER GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. + +0. Additional Definitions. +As used herein, “this License” refers to version 3 of the GNU Lesser General Public License, and the “GNU GPL” refers to version 3 of the GNU General Public License. + +“The Library” refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. + +An “Application” is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. + +A “Combined Work” is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the “Linked Version”. + +The “Minimal Corresponding Source” for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. + +The “Corresponding Application Code” for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. + +1. Exception to Section 3 of the GNU GPL. +You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. + +2. Conveying Modified Versions. +If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: + +a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or +b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. +3. Object Code Incorporating Material from Library Header Files. +The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: + +a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. +b) Accompany the object code with a copy of the GNU GPL and this license document. +4. Combined Works. +You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: + +a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. +b) Accompany the Combined Work with a copy of the GNU GPL and this license document. +c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. +d) Do one of the following: +0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. +1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. +e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) +5. Combined Libraries. +You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: + +a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. +b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. +6. Revised Versions of the GNU Lesser General Public License. +The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. + +If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. + +-------------------------------------------------------------------------------- +7. Rodinia + Copyright (c)2008-2011 University of Virginia +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted without royalty fees or other restrictions, provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of the University of Virginia, the Dept. of Computer Science, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF VIRGINIA OR THE SOFTWARE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +If you use this software or a modified version of it, please cite the most relevant among the following papers: + + - M. A. Goodrum, M. J. Trotter, A. Aksel, S. T. Acton, and K. Skadron. Parallelization of Particle Filter Algorithms. In Proceedings of the 3rd Workshop on Emerging Applications and Many-core Architecture (EAMA), in conjunction with the IEEE/ACM International +Symposium on Computer Architecture (ISCA), June 2010. + + - S. Che, M. Boyer, J. Meng, D. Tarjan, J. W. Sheaffer, Sang-Ha Lee and K. Skadron. +Rodinia: A Benchmark Suite for Heterogeneous Computing. IEEE International Symposium +on Workload Characterization, Oct 2009. + +- J. Meng and K. Skadron. "Performance Modeling and Automatic Ghost Zone Optimization +for Iterative Stencil Loops on GPUs." In Proceedings of the 23rd Annual ACM International +Conference on Supercomputing (ICS), June 2009. + +- L.G. Szafaryn, K. Skadron and J. Saucerman. "Experiences Accelerating MATLAB Systems +Biology Applications." in Workshop on Biomedicine in Computing (BiC) at the International +Symposium on Computer Architecture (ISCA), June 2009. + +- M. Boyer, D. Tarjan, S. T. Acton, and K. Skadron. "Accelerating Leukocyte Tracking using CUDA: +A Case Study in Leveraging Manycore Coprocessors." In Proceedings of the International Parallel +and Distributed Processing Symposium (IPDPS), May 2009. + +- S. Che, M. Boyer, J. Meng, D. Tarjan, J. W. Sheaffer, and K. Skadron. "A Performance +Study of General Purpose Applications on Graphics Processors using CUDA" Journal of +Parallel and Distributed Computing, Elsevier, June 2008. + +-------------------------------------------------------------------------------- +Other names and brands may be claimed as the property of others. + +-------------------------------------------------------------------------------- \ No newline at end of file diff --git a/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/.gitkeep b/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/License.txt b/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/License.txt new file mode 100644 index 0000000000..e63c6e13dc --- /dev/null +++ b/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/License.txt @@ -0,0 +1,7 @@ +Copyright Intel Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/README.md b/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/README.md new file mode 100644 index 0000000000..a8fb984dd9 --- /dev/null +++ b/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/README.md @@ -0,0 +1,140 @@ +# `JAX Getting Started` Sample + +The `JAX Getting Started` sample demonstrates how to train a JAX model and run inference on Intel® hardware. +| Property | Description +|:--- |:--- +| Category | Get Start Sample +| What you will learn | How to start using JAX* on Intel® hardware. +| Time to complete | 10 minutes + +## Purpose + +JAX is a high-performance numerical computing library that enables automatic differentiation. It provides features like just-in-time compilation and efficient parallelization for machine learning and scientific computing tasks. + +This sample code shows how to get started with JAX on CPU. The sample code defines a simple neural network that trains on the MNIST dataset using JAX for parallel computations across multiple CPU cores. The network trains over multiple epochs, evaluates accuracy, and adjusts parameters using stochastic gradient descent across devices. + +## Prerequisites + +| Optimized for | Description +|:--- |:--- +| OS | Ubuntu* 22.0.4 and newer +| Hardware | Intel® Xeon® Scalable processor family +| Software | JAX + +> **Note**: AI and Analytics samples are validated on AI Tools Offline Installer. For the full list of validated platforms refer to [Platform Validation](https://github.com/oneapi-src/oneAPI-samples/tree/master?tab=readme-ov-file#platform-validation). + +## Key Implementation Details + +The getting-started sample code uses the python file 'spmd_mnist_classifier_fromscratch.py' under the examples directory in the +[jax repository](https://github.com/google/jax/). +It implements a simple neural network's training and inference for mnist images. The images are downloaded to a temporary directory when the example is run first. +- **init_random_params** initializes the neural network weights and biases for each layer. +- **predict** computes the forward pass of the network, applying weights, biases, and activations to inputs. +- **loss** calculates the cross-entropy loss between predictions and target labels. +- **spmd_update** performs parallel gradient updates across multiple devices using JAX’s pmap and lax.psum. +- **accuracy** computes the accuracy of the model by predicting the class of each input in the batch and comparing it to the true target class. It uses the *jnp.argmax* function to find the predicted class and then computes the mean of correct predictions. +- **data_stream** function generates batches of shuffled training data. It reshapes the data so that it can be split across multiple cores, ensuring that the batch size is divisible by the number of cores for parallel processing. +- **training loop** trains the model for a set number of epochs, updating parameters and printing training/test accuracy after each epoch. The parameters are replicated across devices and updated in parallel using spmd_update. After each epoch, the model’s accuracy is evaluated on both training and test data using accuracy. + +## Environment Setup + +You will need to download and install the following toolkits, tools, and components to use the sample. + +**1. Get Intel® AI Tools** + +Required AI Tools: 'JAX' +
If you have not already, select and install these Tools via [AI Tools Selector](https://www.intel.com/content/www/us/en/developer/tools/oneapi/ai-tools-selector.html). AI and Analytics samples are validated on AI Tools Offline Installer. It is recommended to select Offline Installer option in AI Tools Selector.
+please see the [supported versions](https://www.intel.com/content/www/us/en/developer/tools/oneapi/ai-tools-selector.html). + +>**Note**: If Docker option is chosen in AI Tools Selector, refer to [Working with Preset Containers](https://github.com/intel/ai-containers/tree/main/preset) to learn how to run the docker and samples. + +**2. (Offline Installer) Activate the AI Tools bundle base environment** + +If the default path is used during the installation of AI Tools: +``` +source $HOME/intel/oneapi/intelpython/bin/activate +``` +If a non-default path is used: +``` +source /bin/activate +``` + +**3. (Offline Installer) Activate relevant Conda environment** + +For the system with Intel CPU: +``` +conda activate jax +``` + +**4. Clone the GitHub repository** +``` +git clone https://github.com/google/jax.git +cd jax +export PYTHONPATH=$PYTHONPATH:$(pwd) +``` +## Run the Sample + +>**Note**: Before running the sample, make sure Environment Setup is completed. +Go to the section which corresponds to the installation method chosen in [AI Tools Selector](https://www.intel.com/content/www/us/en/developer/tools/oneapi/ai-tools-selector.html) to see relevant instructions: +* [AI Tools Offline Installer (Validated)/Conda/PIP](#ai-tools-offline-installer-validatedcondapip) +* [Docker](#docker) +### AI Tools Offline Installer (Validated)/Conda/PIP +``` + python examples/spmd_mnist_classifier_fromscratch.py +``` +### Docker +AI Tools Docker images already have Get Started samples pre-installed. Refer to [Working with Preset Containers](https://github.com/intel/ai-containers/tree/main/preset) to learn how to run the docker and samples. +## Example Output +1. When the program is run, you should see results similar to the following: + +``` +downloaded https://storage.googleapis.com/cvdf-datasets/mnist/train-images-idx3-ubyte.gz to /tmp/jax_example_data/ +downloaded https://storage.googleapis.com/cvdf-datasets/mnist/train-labels-idx1-ubyte.gz to /tmp/jax_example_data/ +downloaded https://storage.googleapis.com/cvdf-datasets/mnist/t10k-images-idx3-ubyte.gz to /tmp/jax_example_data/ +downloaded https://storage.googleapis.com/cvdf-datasets/mnist/t10k-labels-idx1-ubyte.gz to /tmp/jax_example_data/ +Epoch 0 in 2.71 sec +Training set accuracy 0.7381166815757751 +Test set accuracy 0.7516999840736389 +Epoch 1 in 2.35 sec +Training set accuracy 0.81454998254776 +Test set accuracy 0.8277999758720398 +Epoch 2 in 2.33 sec +Training set accuracy 0.8448166847229004 +Test set accuracy 0.8568999767303467 +Epoch 3 in 2.34 sec +Training set accuracy 0.8626833558082581 +Test set accuracy 0.8715999722480774 +Epoch 4 in 2.30 sec +Training set accuracy 0.8752999901771545 +Test set accuracy 0.8816999793052673 +Epoch 5 in 2.33 sec +Training set accuracy 0.8839333653450012 +Test set accuracy 0.8899999856948853 +Epoch 6 in 2.37 sec +Training set accuracy 0.8908833265304565 +Test set accuracy 0.8944999575614929 +Epoch 7 in 2.31 sec +Training set accuracy 0.8964999914169312 +Test set accuracy 0.8986999988555908 +Epoch 8 in 2.28 sec +Training set accuracy 0.9016000032424927 +Test set accuracy 0.9034000039100647 +Epoch 9 in 2.31 sec +Training set accuracy 0.9060333371162415 +Test set accuracy 0.9059999585151672 +``` + +2. Troubleshooting + + If you receive an error message, troubleshoot the problem using the **Diagnostics Utility for Intel® oneAPI Toolkits**. The diagnostic utility provides configuration and system checks to help find missing dependencies, permissions errors, and other issues. See the *[Diagnostics Utility for Intel® oneAPI Toolkits User Guide](https://www.intel.com/content/www/us/en/develop/documentation/diagnostic-utility-user-guide/top.html)* for more information on using the utility + +## License + +Code samples are licensed under the MIT license. See +[License.txt](https://github.com/oneapi-src/oneAPI-samples/blob/master/License.txt) +for details. + +Third party program Licenses can be found here: +[third-party-programs.txt](https://github.com/oneapi-src/oneAPI-samples/blob/master/third-party-programs.txt) + +*Other names and brands may be claimed as the property of others. [Trademarks](https://www.intel.com/content/www/us/en/legal/trademarks.html) diff --git a/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/run.sh b/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/run.sh new file mode 100644 index 0000000000..2a8313d002 --- /dev/null +++ b/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/run.sh @@ -0,0 +1,6 @@ +source $HOME/intel/oneapi/intelpython/bin/activate +conda activate jax +git clone https://github.com/google/jax.git +cd jax +export PYTHONPATH=$PYTHONPATH:$(pwd) +python examples/spmd_mnist_classifier_fromscratch.py diff --git a/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/sample.json b/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/sample.json new file mode 100644 index 0000000000..96c1fffd5b --- /dev/null +++ b/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/sample.json @@ -0,0 +1,24 @@ +{ + "guid": "9A6A140B-FBD0-4CB2-849A-9CAF15A6F3B1", + "name": "Getting Started example for JAX CPU", + "categories": ["Toolkit/oneAPI AI And Analytics/Getting Started"], + "description": "This sample illustrates how to train a JAX model and run inference", + "builder": ["cli"], + "languages": [{ + "python": {} + }], + "os": ["linux"], + "targetDevice": ["CPU"], + "ciTests": { + "linux": [{ + "id": "JAX CPU example", + "steps": [ + "git clone https://github.com/google/jax.git", + "cd jax", + "conda activate jax", + "python examples/spmd_mnist_classifier_fromscratch.py" + ] + }] + }, + "expertise": "Getting Started" +} diff --git a/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/third-party-programs.txt b/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/third-party-programs.txt new file mode 100644 index 0000000000..e9f8042d0a --- /dev/null +++ b/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/third-party-programs.txt @@ -0,0 +1,253 @@ +oneAPI Code Samples - Third Party Programs File + +This file contains the list of third party software ("third party programs") +contained in the Intel software and their required notices and/or license +terms. This third party software, even if included with the distribution of the +Intel software, may be governed by separate license terms, including without +limitation, third party license terms, other Intel software license terms, and +open source software license terms. These separate license terms govern your use +of the third party programs as set forth in the “third-party-programs.txt” or +other similarly named text file. + +Third party programs and their corresponding required notices and/or license +terms are listed below. + +-------------------------------------------------------------------------------- + +1. Nothings STB Libraries + +stb/LICENSE + + This software is available under 2 licenses -- choose whichever you prefer. + ------------------------------------------------------------------------------ + ALTERNATIVE A - MIT License + Copyright (c) 2017 Sean Barrett + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is furnished to do + so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + ------------------------------------------------------------------------------ + ALTERNATIVE B - Public Domain (www.unlicense.org) + This is free and unencumbered software released into the public domain. + Anyone is free to copy, modify, publish, use, compile, sell, or distribute this + software, either in source code form or as a compiled binary, for any purpose, + commercial or non-commercial, and by any means. + In jurisdictions that recognize copyright laws, the author or authors of this + software dedicate any and all copyright interest in the software to the public + domain. We make this dedication for the benefit of the public at large and to + the detriment of our heirs and successors. We intend this dedication to be an + overt act of relinquishment in perpetuity of all present and future rights to + this software under copyright law. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +-------------------------------------------------------------------------------- + +2. FGPA example designs-gzip + + SDL2.0 + +zlib License + + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + +-------------------------------------------------------------------------------- + +3. Nbody + (c) 2019 Fabio Baruffa + + Plotly.js + Copyright (c) 2020 Plotly, Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +© 2020 GitHub, Inc. + +-------------------------------------------------------------------------------- + +4. GNU-EFI + Copyright (c) 1998-2000 Intel Corporation + +The files in the "lib" and "inc" subdirectories are using the EFI Application +Toolkit distributed by Intel at http://developer.intel.com/technology/efi + +This code is covered by the following agreement: + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INTEL BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. THE EFI SPECIFICATION AND ALL OTHER INFORMATION +ON THIS WEB SITE ARE PROVIDED "AS IS" WITH NO WARRANTIES, AND ARE SUBJECT +TO CHANGE WITHOUT NOTICE. + +-------------------------------------------------------------------------------- + +5. Edk2 + Copyright (c) 2019, Intel Corporation. All rights reserved. + + Edk2 Basetools + Copyright (c) 2019, Intel Corporation. All rights reserved. + +SPDX-License-Identifier: BSD-2-Clause-Patent + +-------------------------------------------------------------------------------- + +6. Heat Transmission + +GNU LESSER GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. + +0. Additional Definitions. +As used herein, “this License” refers to version 3 of the GNU Lesser General Public License, and the “GNU GPL” refers to version 3 of the GNU General Public License. + +“The Library” refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. + +An “Application” is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. + +A “Combined Work” is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the “Linked Version”. + +The “Minimal Corresponding Source” for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. + +The “Corresponding Application Code” for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. + +1. Exception to Section 3 of the GNU GPL. +You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. + +2. Conveying Modified Versions. +If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: + +a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or +b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. +3. Object Code Incorporating Material from Library Header Files. +The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: + +a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. +b) Accompany the object code with a copy of the GNU GPL and this license document. +4. Combined Works. +You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: + +a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. +b) Accompany the Combined Work with a copy of the GNU GPL and this license document. +c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. +d) Do one of the following: +0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. +1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. +e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) +5. Combined Libraries. +You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: + +a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. +b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. +6. Revised Versions of the GNU Lesser General Public License. +The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. + +If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. + +-------------------------------------------------------------------------------- +7. Rodinia + Copyright (c)2008-2011 University of Virginia +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted without royalty fees or other restrictions, provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of the University of Virginia, the Dept. of Computer Science, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF VIRGINIA OR THE SOFTWARE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +If you use this software or a modified version of it, please cite the most relevant among the following papers: + + - M. A. Goodrum, M. J. Trotter, A. Aksel, S. T. Acton, and K. Skadron. Parallelization of Particle Filter Algorithms. In Proceedings of the 3rd Workshop on Emerging Applications and Many-core Architecture (EAMA), in conjunction with the IEEE/ACM International +Symposium on Computer Architecture (ISCA), June 2010. + + - S. Che, M. Boyer, J. Meng, D. Tarjan, J. W. Sheaffer, Sang-Ha Lee and K. Skadron. +Rodinia: A Benchmark Suite for Heterogeneous Computing. IEEE International Symposium +on Workload Characterization, Oct 2009. + +- J. Meng and K. Skadron. "Performance Modeling and Automatic Ghost Zone Optimization +for Iterative Stencil Loops on GPUs." In Proceedings of the 23rd Annual ACM International +Conference on Supercomputing (ICS), June 2009. + +- L.G. Szafaryn, K. Skadron and J. Saucerman. "Experiences Accelerating MATLAB Systems +Biology Applications." in Workshop on Biomedicine in Computing (BiC) at the International +Symposium on Computer Architecture (ISCA), June 2009. + +- M. Boyer, D. Tarjan, S. T. Acton, and K. Skadron. "Accelerating Leukocyte Tracking using CUDA: +A Case Study in Leveraging Manycore Coprocessors." In Proceedings of the International Parallel +and Distributed Processing Symposium (IPDPS), May 2009. + +- S. Che, M. Boyer, J. Meng, D. Tarjan, J. W. Sheaffer, and K. Skadron. "A Performance +Study of General Purpose Applications on Graphics Processors using CUDA" Journal of +Parallel and Distributed Computing, Elsevier, June 2008. + +-------------------------------------------------------------------------------- +Other names and brands may be claimed as the property of others. + +-------------------------------------------------------------------------------- \ No newline at end of file diff --git a/AI-and-Analytics/Getting-Started-Samples/README.md b/AI-and-Analytics/Getting-Started-Samples/README.md index 4aa716713c..14154dc9fd 100644 --- a/AI-and-Analytics/Getting-Started-Samples/README.md +++ b/AI-and-Analytics/Getting-Started-Samples/README.md @@ -27,5 +27,6 @@ Third party program Licenses can be found here: [third-party-programs.txt](https |Classical Machine Learning| Scikit-learn (OneDAL) | [Intel_Extension_For_SKLearn_GettingStarted](Intel_Extension_For_SKLearn_GettingStarted) | Speed up a scikit-learn application using Intel oneDAL. |Deep Learning
Inference Optimization|Intel® Extension of TensorFlow | [Intel® Extension For TensorFlow GettingStarted](Intel_Extension_For_TensorFlow_GettingStarted) | Guides users how to run a TensorFlow inference workload on both GPU and CPU. |Deep Learning Inference Optimization|oneCCL Bindings for PyTorch | [Intel oneCCL Bindings For PyTorch GettingStarted](Intel_oneCCL_Bindings_For_PyTorch_GettingStarted) | Guides users through the process of running a simple PyTorch* distributed workload on both GPU and CPU. | +|Inference Optimization|JAX Getting Started Sample | [IntelJAX GettingStarted](https://github.com/oneapi-src/oneAPI-samples/tree/development/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted) | The JAX Getting Started sample demonstrates how to train a JAX model and run inference on Intel® hardware. | *Other names and brands may be claimed as the property of others. [Trademarks](https://www.intel.com/content/www/us/en/legal/trademarks.html) From 9350d1877e6e376339e7cf92ba98175491c8e613 Mon Sep 17 00:00:00 2001 From: alexsin368 Date: Tue, 3 Dec 2024 17:59:07 -0800 Subject: [PATCH 2/7] adding lang ID modernization changes --- .../Dataset/get_dataset.py | 35 +++ .../LanguageIdentification/Inference/clean.sh | 6 +- .../Inference/inference_commonVoice.py | 10 +- .../Inference/inference_custom.py | 67 +++-- .../Inference/initialize.sh | 23 -- .../Inference/interfaces.patch | 11 - .../Inference/lang_id_inference.ipynb | 46 +++- .../Inference/quantize_model.py | 4 +- .../Inference/sample_input_features.pt | Bin 48939 -> 0 bytes .../Inference/sample_wavs.pt | Bin 0 -> 129200 bytes .../LanguageIdentification/README.md | 259 +++++++++--------- .../LanguageIdentification/Training/clean.sh | 5 +- .../Training/create_wds_shards.patch | 10 +- .../Training/initialize.sh | 26 -- .../Training/lang_id_training.ipynb | 72 +++-- .../Training/prepareAllCommonVoice.py | 6 +- .../Training/train_ecapa.patch | 46 ++-- .../LanguageIdentification/initialize.sh | 23 ++ .../LanguageIdentification/launch_docker.sh | 13 - .../LanguageIdentification/sample.json | 11 + 20 files changed, 385 insertions(+), 288 deletions(-) create mode 100644 AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Dataset/get_dataset.py delete mode 100644 AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/initialize.sh delete mode 100644 AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/interfaces.patch delete mode 100644 AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/sample_input_features.pt create mode 100644 AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/sample_wavs.pt delete mode 100644 AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/initialize.sh create mode 100644 AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/initialize.sh delete mode 100644 AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/launch_docker.sh diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Dataset/get_dataset.py b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Dataset/get_dataset.py new file mode 100644 index 0000000000..f30a8d06e7 --- /dev/null +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Dataset/get_dataset.py @@ -0,0 +1,35 @@ +import os +import shutil +import argparse +from datasets import load_dataset +from tqdm import tqdm + +language_to_code = { + "japanese": "ja", + "swedish": "sv-SE" +} + +def download_dataset(output_dir): + for lang, lang_code in language_to_code.items(): + print(f"Processing dataset for language: {lang_code}") + + # Load the dataset for the specific language + dataset = load_dataset("mozilla-foundation/common_voice_11_0", lang_code, split="train", trust_remote_code=True) + + # Create a language-specific output folder + output_folder = os.path.join(output_dir, lang, lang_code, "clips") + os.makedirs(output_folder, exist_ok=True) + + # Extract and copy MP3 files + for sample in tqdm(dataset, desc=f"Extracting and copying MP3 files for {lang}"): + audio_path = sample['audio']['path'] + shutil.copy(audio_path, output_folder) + + print("Extraction and copy complete.") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Extract and copy audio files from a dataset to a specified directory.") + parser.add_argument("--output_dir", type=str, default="/data/commonVoice", help="Base output directory for saving the files. Default is /data/commonVoice") + args = parser.parse_args() + + download_dataset(args.output_dir) \ No newline at end of file diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/clean.sh b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/clean.sh index 7ea1719af4..34747af45c 100644 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/clean.sh +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/clean.sh @@ -1,7 +1,5 @@ #!/bin/bash -rm -R RIRS_NOISES -rm -R tmp -rm -R speechbrain -rm -f rirs_noises.zip noise.csv reverb.csv vad_file.txt +echo "Deleting .wav files, tmp" rm -f ./*.wav +rm -R tmp diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/inference_commonVoice.py b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/inference_commonVoice.py index 6442418bf0..7effb2df76 100644 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/inference_commonVoice.py +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/inference_commonVoice.py @@ -29,7 +29,7 @@ def __init__(self, dirpath, filename): self.sampleRate = 0 self.waveData = '' self.wavesize = 0 - self.waveduriation = 0 + self.waveduration = 0 if filename.endswith(".wav") or filename.endswith(".wmv"): self.wavefile = filename self.wavepath = dirpath + os.sep + filename @@ -173,12 +173,12 @@ def main(argv): data = datafile(testDataDirectory, filename) predict_list = [] use_entire_audio_file = False - if data.waveduration < sample_dur: + if int(data.waveduration) <= sample_dur: # Use entire audio file if the duration is less than the sampling duration use_entire_audio_file = True sample_list = [0 for _ in range(sample_size)] else: - start_time_list = list(range(sample_size - int(data.waveduration) + 1)) + start_time_list = list(range(0, int(data.waveduration) - sample_dur)) sample_list = [] for i in range(sample_size): sample_list.append(random.sample(start_time_list, 1)[0]) @@ -198,10 +198,6 @@ def main(argv): predict_list.append(' ') pass - # Clean up - if use_entire_audio_file: - os.remove("./" + data.filename) - # Pick the top rated prediction result occurence_count = Counter(predict_list) total_count = sum(occurence_count.values()) diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/inference_custom.py b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/inference_custom.py index b4f9d6adee..2b4a331c0b 100644 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/inference_custom.py +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/inference_custom.py @@ -30,7 +30,7 @@ def __init__(self, dirpath, filename): self.sampleRate = 0 self.waveData = '' self.wavesize = 0 - self.waveduriation = 0 + self.waveduration = 0 if filename.endswith(".wav") or filename.endswith(".wmv"): self.wavefile = filename self.wavepath = dirpath + os.sep + filename @@ -61,41 +61,45 @@ def __init__(self, ipex_op=False, bf16=False, int8_model=False): self.model_int8 = load(source_model_int8_path, self.language_id) self.model_int8.eval() elif ipex_op: + self.language_id.eval() + # Optimize for inference with IPEX print("Optimizing inference with IPEX") - self.language_id.eval() - sampleInput = (torch.load("./sample_input_features.pt"), torch.load("./sample_input_wav_lens.pt")) if bf16: print("BF16 enabled") self.language_id.mods["compute_features"] = ipex.optimize(self.language_id.mods["compute_features"], dtype=torch.bfloat16) self.language_id.mods["mean_var_norm"] = ipex.optimize(self.language_id.mods["mean_var_norm"], dtype=torch.bfloat16) - self.language_id.mods["embedding_model"] = ipex.optimize(self.language_id.mods["embedding_model"], dtype=torch.bfloat16) self.language_id.mods["classifier"] = ipex.optimize(self.language_id.mods["classifier"], dtype=torch.bfloat16) else: self.language_id.mods["compute_features"] = ipex.optimize(self.language_id.mods["compute_features"]) self.language_id.mods["mean_var_norm"] = ipex.optimize(self.language_id.mods["mean_var_norm"]) - self.language_id.mods["embedding_model"] = ipex.optimize(self.language_id.mods["embedding_model"]) self.language_id.mods["classifier"] = ipex.optimize(self.language_id.mods["classifier"]) # Torchscript to resolve performance issues with reorder operations + print("Applying Torchscript") + sampleWavs = torch.load("./sample_wavs.pt") + sampleWavLens = torch.ones(sampleWavs.shape[0]) with torch.no_grad(): - I2 = self.language_id.mods["embedding_model"](*sampleInput) + I1 = self.language_id.mods["compute_features"](sampleWavs) + I2 = self.language_id.mods["mean_var_norm"](I1, sampleWavLens) + I3 = self.language_id.mods["embedding_model"](I2, sampleWavLens) + if bf16: with torch.cpu.amp.autocast(): - self.language_id.mods["compute_features"] = torch.jit.trace( self.language_id.mods["compute_features"] , example_inputs=(torch.rand(1,32000))) - self.language_id.mods["mean_var_norm"] = torch.jit.trace(self.language_id.mods["mean_var_norm"], example_inputs=sampleInput) - self.language_id.mods["embedding_model"] = torch.jit.trace(self.language_id.mods["embedding_model"], example_inputs=sampleInput) - self.language_id.mods["classifier"] = torch.jit.trace(self.language_id.mods["classifier"], example_inputs=I2) + self.language_id.mods["compute_features"] = torch.jit.trace( self.language_id.mods["compute_features"] , example_inputs=sampleWavs) + self.language_id.mods["mean_var_norm"] = torch.jit.trace(self.language_id.mods["mean_var_norm"], example_inputs=(I1, sampleWavLens)) + self.language_id.mods["embedding_model"] = torch.jit.trace(self.language_id.mods["embedding_model"], example_inputs=(I2, sampleWavLens)) + self.language_id.mods["classifier"] = torch.jit.trace(self.language_id.mods["classifier"], example_inputs=I3) self.language_id.mods["compute_features"] = torch.jit.freeze(self.language_id.mods["compute_features"]) self.language_id.mods["mean_var_norm"] = torch.jit.freeze(self.language_id.mods["mean_var_norm"]) self.language_id.mods["embedding_model"] = torch.jit.freeze(self.language_id.mods["embedding_model"]) self.language_id.mods["classifier"] = torch.jit.freeze( self.language_id.mods["classifier"]) else: - self.language_id.mods["compute_features"] = torch.jit.trace( self.language_id.mods["compute_features"] , example_inputs=(torch.rand(1,32000))) - self.language_id.mods["mean_var_norm"] = torch.jit.trace(self.language_id.mods["mean_var_norm"], example_inputs=sampleInput) - self.language_id.mods["embedding_model"] = torch.jit.trace(self.language_id.mods["embedding_model"], example_inputs=sampleInput) - self.language_id.mods["classifier"] = torch.jit.trace(self.language_id.mods["classifier"], example_inputs=I2) + self.language_id.mods["compute_features"] = torch.jit.trace( self.language_id.mods["compute_features"] , example_inputs=sampleWavs) + self.language_id.mods["mean_var_norm"] = torch.jit.trace(self.language_id.mods["mean_var_norm"], example_inputs=(I1, sampleWavLens)) + self.language_id.mods["embedding_model"] = torch.jit.trace(self.language_id.mods["embedding_model"], example_inputs=(I2, sampleWavLens)) + self.language_id.mods["classifier"] = torch.jit.trace(self.language_id.mods["classifier"], example_inputs=I3) self.language_id.mods["compute_features"] = torch.jit.freeze(self.language_id.mods["compute_features"]) self.language_id.mods["mean_var_norm"] = torch.jit.freeze(self.language_id.mods["mean_var_norm"]) @@ -114,11 +118,11 @@ def predict(self, data_path="", ipex_op=False, bf16=False, int8_model=False, ver with torch.no_grad(): if bf16: with torch.cpu.amp.autocast(): - prediction = self.language_id.classify_batch(signal) + prediction = self.language_id.classify_batch(signal) else: - prediction = self.language_id.classify_batch(signal) + prediction = self.language_id.classify_batch(signal) else: # default - prediction = self.language_id.classify_batch(signal) + prediction = self.language_id.classify_batch(signal) inference_end_time = time() inference_latency = inference_end_time - inference_start_time @@ -195,13 +199,13 @@ def main(argv): with open(OUTPUT_SUMMARY_CSV_FILE, 'w') as f: writer = csv.writer(f) writer.writerow(["Audio File", - "Input Frequency", + "Input Frequency (Hz)", "Expected Language", "Top Consensus", "Top Consensus %", "Second Consensus", "Second Consensus %", - "Average Latency", + "Average Latency (s)", "Result"]) total_samples = 0 @@ -273,12 +277,12 @@ def main(argv): predict_list = [] use_entire_audio_file = False latency_sum = 0.0 - if data.waveduration < sample_dur: + if int(data.waveduration) <= sample_dur: # Use entire audio file if the duration is less than the sampling duration use_entire_audio_file = True sample_list = [0 for _ in range(sample_size)] else: - start_time_list = list(range(sample_size - int(data.waveduration) + 1)) + start_time_list = list(range(int(data.waveduration) - sample_dur)) sample_list = [] for i in range(sample_size): sample_list.append(random.sample(start_time_list, 1)[0]) @@ -346,17 +350,36 @@ def main(argv): avg_latency, result ]) + else: + # Write results to a .csv file + with open(OUTPUT_SUMMARY_CSV_FILE, 'a') as f: + writer = csv.writer(f) + writer.writerow([ + filename, + sample_rate_for_csv, + "N/A", + top_occurance, + str(topPercentage) + "%", + sec_occurance, + str(secPercentage) + "%", + avg_latency, + "N/A" + ]) + if ground_truth_compare: # Summary of results print("\n\n Correctly predicted %d/%d\n" %(correct_predictions, total_samples)) - print("\n See %s for summary\n" %(OUTPUT_SUMMARY_CSV_FILE)) + + print("\n See %s for summary\n" %(OUTPUT_SUMMARY_CSV_FILE)) elif os.path.isfile(path): print("\nIt is a normal file", path) else: print("It is a special file (socket, FIFO, device file)" , path) + print("Done.\n") + if __name__ == "__main__": import sys sys.exit(main(sys.argv)) diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/initialize.sh b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/initialize.sh deleted file mode 100644 index 935debac44..0000000000 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/initialize.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -# Activate the oneAPI environment for PyTorch -source activate pytorch - -# Install speechbrain -git clone https://github.com/speechbrain/speechbrain.git -cd speechbrain -pip install -r requirements.txt -pip install --editable . -cd .. - -# Add speechbrain to environment variable PYTHONPATH -export PYTHONPATH=$PYTHONPATH:/Inference/speechbrain - -# Install PyTorch and Intel Extension for PyTorch (IPEX) -pip install torch==1.13.1 torchaudio -pip install --no-deps torchvision==0.14.0 -pip install intel_extension_for_pytorch==1.13.100 -pip install neural-compressor==2.0 - -# Update packages -apt-get update && apt-get install libgl1 diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/interfaces.patch b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/interfaces.patch deleted file mode 100644 index 762ae5ebee..0000000000 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/interfaces.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- interfaces.py 2022-10-07 16:58:26.836359346 -0700 -+++ interfaces_new.py 2022-10-07 16:59:09.968110128 -0700 -@@ -945,7 +945,7 @@ - out_prob = self.mods.classifier(emb).squeeze(1) - score, index = torch.max(out_prob, dim=-1) - text_lab = self.hparams.label_encoder.decode_torch(index) -- return out_prob, score, index, text_lab -+ return out_prob, score, index # removed text_lab to get torchscript to work - - def classify_file(self, path): - """Classifies the given audiofile into the given set of labels. diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/lang_id_inference.ipynb b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/lang_id_inference.ipynb index 0ed44139b3..1cd1afee01 100644 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/lang_id_inference.ipynb +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/lang_id_inference.ipynb @@ -47,7 +47,7 @@ "metadata": {}, "outputs": [], "source": [ - "!python inference_commonVoice.py -p /data/commonVoice/test" + "!python inference_commonVoice.py -p ${COMMON_VOICE_PATH}/processed_data/test" ] }, { @@ -55,7 +55,7 @@ "metadata": {}, "source": [ "## inference_custom.py for Custom Data \n", - "To generate an overall results output summary, the audio_ground_truth_labels.csv file needs to be modified with the name of the audio file and expected audio label (i.e. en for English). By default, this is disabled but if desired, the *--ground_truth_compare* can be used. To run inference on custom data, you must specify a folder with WAV files and pass the path in as an argument. " + "To run inference on custom data, you must specify a folder with .wav files and pass the path in as an argument. You can do so by creating a folder named `data_custom` and then copy 1 or 2 .wav files from your test dataset into it. .mp3 files will NOT work. " ] }, { @@ -65,7 +65,7 @@ "### Randomly select audio clips from audio files for prediction\n", "python inference_custom.py -p DATAPATH -d DURATION -s SIZE\n", "\n", - "An output file output_summary.csv will give the summary of the results." + "An output file `output_summary.csv` will give the summary of the results." ] }, { @@ -104,6 +104,8 @@ "### Optimizations with Intel® Extension for PyTorch (IPEX) \n", "python inference_custom.py -p data_custom -d 3 -s 50 --vad --ipex --verbose \n", "\n", + "This will apply ipex.optimize to the model(s) and TorchScript. You can also add the --bf16 option along with --ipex to run in the BF16 data type, supported on 4th Gen Intel® Xeon® Scalable processors and newer.\n", + "\n", "Note that the *--verbose* option is required to view the latency measurements. " ] }, @@ -121,7 +123,7 @@ "metadata": {}, "source": [ "## Quantization with Intel® Neural Compressor (INC)\n", - "To improve inference latency, Intel® Neural Compressor (INC) can be used to quantize the trained model from FP32 to INT8 by running quantize_model.py. The *-datapath* argument can be used to specify a custom evaluation dataset but by default it is set to */data/commonVoice/dev* which was generated from the data preprocessing scripts in the *Training* folder. " + "To improve inference latency, Intel® Neural Compressor (INC) can be used to quantize the trained model from FP32 to INT8 by running quantize_model.py. The *-datapath* argument can be used to specify a custom evaluation dataset but by default it is set to `$COMMON_VOICE_PATH/processed_data/dev` which was generated from the data preprocessing scripts in the `Training` folder. " ] }, { @@ -130,14 +132,46 @@ "metadata": {}, "outputs": [], "source": [ - "!python quantize_model.py -p ./lang_id_commonvoice_model -datapath $COMMON_VOICE_PATH/dev" + "!python quantize_model.py -p ./lang_id_commonvoice_model -datapath $COMMON_VOICE_PATH/processed_data/dev" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After quantization, the model will be stored in lang_id_commonvoice_model_INT8 and neural_compressor.utils.pytorch.load will have to be used to load the quantized model for inference. If self.language_id is the original model and data_path is the path to the audio file:\n", + "\n", + "```\n", + "from neural_compressor.utils.pytorch import load\n", + "model_int8 = load(\"./lang_id_commonvoice_model_INT8\", self.language_id)\n", + "signal = self.language_id.load_audio(data_path)\n", + "prediction = self.model_int8(signal)\n", + "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "After quantization, the model will be stored in *lang_id_commonvoice_model_INT8* and *neural_compressor.utils.pytorch.load* will have to be used to load the quantized model for inference. " + "The code above is integrated into inference_custom.py. You can now run inference on your data using this INT8 model:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!python inference_custom.py -p data_custom -d 3 -s 50 --vad --int8_model --verbose" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### (Optional) Comparing Predictions with Ground Truth\n", + "\n", + "You can choose to modify audio_ground_truth_labels.csv to include the name of the audio file and expected audio label (like, en for English), then run inference_custom.py with the --ground_truth_compare option. By default, this is disabled." ] }, { diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/quantize_model.py b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/quantize_model.py index 428e24142e..e5ce7f9bbc 100644 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/quantize_model.py +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/quantize_model.py @@ -18,8 +18,6 @@ from neural_compressor.utils.pytorch import load from speechbrain.pretrained import EncoderClassifier -DEFAULT_EVAL_DATA_PATH = "/data/commonVoice/dev" - def prepare_dataset(path): data_list = [] for dir_name in os.listdir(path): @@ -33,7 +31,7 @@ def main(argv): import argparse parser = argparse.ArgumentParser() parser.add_argument('-p', type=str, required=True, help="Path to the model to be optimized") - parser.add_argument('-datapath', type=str, default=DEFAULT_EVAL_DATA_PATH, help="Path to evaluation dataset") + parser.add_argument('-datapath', type=str, required=True, help="Path to evaluation dataset") args = parser.parse_args() model_path = args.p diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/sample_input_features.pt b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/sample_input_features.pt deleted file mode 100644 index 61114fe706f7f1164a8508ee6367bc119989b427..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48939 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 E1rp{^yZ`_I diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/sample_wavs.pt b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/sample_wavs.pt new file mode 100644 index 0000000000000000000000000000000000000000..72ea7cc6590ec92c73bd195f6782b1fe3741be81 GIT binary patch literal 129200 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 bJ-UDYuk*jp9&VC Hugging Face SpeechBrain +| OS | Ubuntu* 22.04 or newer +| Hardware | Intel® Xeon® and Core® processor families +| Software | Intel® AI Tools
Hugging Face SpeechBrain ## Key Implementation Details The [CommonVoice](https://commonvoice.mozilla.org/) dataset is used to train an Emphasized Channel Attention, Propagation and Aggregation Time Delay Neural Network (ECAPA-TDNN). This is implemented in the [Hugging Face SpeechBrain](https://huggingface.co/SpeechBrain) library. Additionally, a small Convolutional Recurrent Deep Neural Network (CRDNN) pretrained on the LibriParty dataset is used to process audio samples and output the segments where speech activity is detected. -After you have downloaded the CommonVoice dataset, the data must be preprocessed by converting the MP3 files into WAV format and separated into training, validation, and testing sets. - The model is then trained from scratch using the Hugging Face SpeechBrain library. This model is then used for inference on the testing dataset or a user-specified dataset. There is an option to utilize SpeechBrain's Voice Activity Detection (VAD) where only the speech segments from the audio files are extracted and combined before samples are randomly selected as input into the model. To improve performance, the user may quantize the trained model to INT8 using Intel® Neural Compressor (INC) to decrease latency. The sample contains three discreet phases: @@ -39,93 +37,94 @@ For both training and inference, you can run the sample and scripts in Jupyter N ## Prepare the Environment -### Downloading the CommonVoice Dataset +### Create and Set Up Environment ->**Note**: You can skip downloading the dataset if you already have a pretrained model and only want to run inference on custom data samples that you provide. +1. Create your conda environment by following the instructions on the Intel [AI Tools Selector](https://www.intel.com/content/www/us/en/developer/tools/oneapi/ai-tools-selector.html). You can follow these settings: -Download the CommonVoice dataset for languages of interest from [https://commonvoice.mozilla.org/en/datasets](https://commonvoice.mozilla.org/en/datasets). +* Tool: AI Tools +* Preset or customize: Customize +* Distribution Type: conda* or pip +* Python Versions: Python* 3.9 or 3.10 +* PyTorch* Framework Optimizations: Intel® Extension for PyTorch* (CPU) +* Intel®-Optimized Tools & Libraries: Intel® Neural Compressor -For this sample, you will need to download the following languages: **Japanese** and **Swedish**. Follow Steps 1-6 below or you can execute the code. +>**Note**: Be sure to activate your environment before installing the packages. If using pip, install using `python -m pip` instead of just `pip`. + +2. Create your dataset folder and set the environment variable `COMMON_VOICE_PATH`. This needs to match with where you downloaded your dataset. +```bash +mkdir -p /data/commonVoice +export COMMON_VOICE_PATH=/data/commonVoice +``` -1. On the CommonVoice website, select the Version and Language. -2. Enter your email. -3. Check the boxes, and right-click on the download button to copy the link address. -4. Paste this link into a text editor and copy the first part of the URL up to ".tar.gz". -5. Use **GNU wget** on the URL to download the data to `/data/commonVoice`. +3. Install packages needed for MP3 to WAV conversion +```bash +sudo apt-get update && apt-get install -y ffmpeg libgl1 +``` - Alternatively, you can use a directory on your local drive (due to the large amount of data). If you opt to do so, you must change the `COMMON_VOICE_PATH` environment in `launch_docker.sh` before running the script. +4. Navigate to your working directory, clone the `oneapi-src` repository, and navigate to this code sample. +```bash +git clone https://github.com/oneapi-src/oneAPI-samples.git +cd oneAPI-samples/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification +``` -6. Extract the compressed folder, and rename the folder with the language (for example, English). +5. Run the bash script to install additional necessary libraries, including SpeechBrain. +```bash +source initialize.sh +``` - The file structure **must match** the `LANGUAGE_PATHS` defined in `prepareAllCommonVoice.py` in the `Training` folder for the script to run properly. +### Download the CommonVoice Dataset -These commands illustrate Steps 1-6. Notice that it downloads Japanese and Swedish from CommonVoice version 11.0. +>**Note**: You can skip downloading the dataset if you already have a pretrained model and only want to run inference on custom data samples that you provide. + +First, change to the `Dataset` directory. ``` -# Create the commonVoice directory under 'data' -sudo chmod 777 -R /data -cd /data -mkdir commonVoice -cd commonVoice - -# Download the CommonVoice data -wget \ -https://mozilla-common-voice-datasets.s3.dualstack.us-west-2.amazonaws.com/cv-corpus-11.0-2022-09-21/cv-corpus-11.0-2022-09-21-ja.tar.gz \ -https://mozilla-common-voice-datasets.s3.dualstack.us-west-2.amazonaws.com/cv-corpus-11.0-2022-09-21/cv-corpus-11.0-2022-09-21-sv-SE.tar.gz - -# Extract and organize the CommonVoice data into respective folders by language -tar -xf cv-corpus-11.0-2022-09-21-ja.tar.gz -mv cv-corpus-11.0-2022-09-21 japanese -tar -xf cv-corpus-11.0-2022-09-21-sv-SE.tar.gz -mv cv-corpus-11.0-2022-09-21 swedish +cd ./Dataset ``` -### Configuring the Container +The `get_dataset.py` script downloads the Common Voice dataset by doing the following: -1. Pull the `oneapi-aikit` docker image. -2. Set up the Docker environment. - ``` - docker pull intel/oneapi-aikit - ./launch_docker.sh - ``` - >**Note**: By default, the `Inference` and `Training` directories will be mounted and the environment variable `COMMON_VOICE_PATH` will be set to `/data/commonVoice` and mounted to `/data`. `COMMON_VOICE_PATH` is the location of where the CommonVoice dataset is downloaded. +- Gets the train set of the [Common Voice dataset from Huggingface](https://huggingface.co/datasets/mozilla-foundation/common_voice_11_0) for Japanese and Swedish +- Downloads each mp3 and moves them to the `output_dir` folder +1. If you want to add additional languages, then modify the `language_to_code` dictionary in the file to reflect the languages to be included in the model. +3. Run the script with options. + ```bash + python get_dataset.py --output_dir ${COMMON_VOICE_PATH} + ``` + | Parameters | Description + |:--- |:--- + | `--output_dir` | Base output directory for saving the files. Default is /data/commonVoice + +Once the dataset is downloaded, navigate back to the parent directory +``` +cd .. +``` ## Train the Model with Languages This section explains how to train a model for language identification using the CommonVoice dataset, so it includes steps on how to preprocess the data, train the model, and prepare the output files for inference. -### Configure the Training Environment - -1. Change to the `Training` directory. - ``` - cd /Training - ``` -2. Source the bash script to install the necessary components. - ``` - source initialize.sh - ``` - This installs PyTorch*, the Intel® Extension for PyTorch (IPEX), and other components. +First, change to the `Training` directory. +``` +cd ./Training +``` -### Run in Jupyter Notebook +### Option 1: Run in Jupyter Notebook -1. Install Jupyter Notebook. - ``` - pip install notebook - ``` -2. Launch Jupyter Notebook. +1. Launch Jupyter Notebook. ``` jupyter notebook --ip 0.0.0.0 --port 8888 --allow-root ``` -3. Follow the instructions to open the URL with the token in your browser. -4. Locate and select the Training Notebook. +2. Follow the instructions to open the URL with the token in your browser. +3. Locate and select the Training Notebook. ``` lang_id_training.ipynb ``` -5. Follow the instructions in the Notebook. +4. Follow the instructions in the Notebook. -### Run in a Console +### Option 2: Run in a Console If you cannot or do not want to use Jupyter Notebook, use these procedures to run the sample and scripts locally. @@ -133,13 +132,13 @@ If you cannot or do not want to use Jupyter Notebook, use these procedures to ru 1. Acquire copies of the training scripts. (The command retrieves copies of the required VoxLingua107 training scripts from SpeechBrain.) ``` - cp speechbrain/recipes/VoxLingua107/lang_id/create_wds_shards.py create_wds_shards.py - cp speechbrain/recipes/VoxLingua107/lang_id/train.py train.py - cp speechbrain/recipes/VoxLingua107/lang_id/hparams/train_ecapa.yaml train_ecapa.yaml + cp ../speechbrain/recipes/VoxLingua107/lang_id/create_wds_shards.py create_wds_shards.py + cp ../speechbrain/recipes/VoxLingua107/lang_id/train.py train.py + cp ../speechbrain/recipes/VoxLingua107/lang_id/hparams/train_ecapa.yaml train_ecapa.yaml ``` 2. From the `Training` directory, apply patches to modify these files to work with the CommonVoice dataset. - ``` + ```bash patch < create_wds_shards.patch patch < train_ecapa.patch ``` @@ -154,8 +153,8 @@ The `prepareAllCommonVoice.py` script performs the following data preprocessing 1. If you want to add additional languages, then modify the `LANGUAGE_PATHS` list in the file to reflect the languages to be included in the model. 2. Run the script with options. The samples will be divided as follows: 80% training, 10% validation, 10% testing. - ``` - python prepareAllCommonVoice.py -path /data -max_samples 2000 --createCsv --train --dev --test + ```bash + python prepareAllCommonVoice.py -path $COMMON_VOICE_PATH -max_samples 2000 --createCsv --train --dev --test ``` | Parameters | Description |:--- |:--- @@ -166,27 +165,28 @@ The `prepareAllCommonVoice.py` script performs the following data preprocessing #### Create Shards for Training and Validation -1. If the `/data/commonVoice_shards` folder exists, delete the folder and the contents before proceeding. +1. If the `${COMMON_VOICE_PATH}/processed_data/commonVoice_shards` folder exists, delete the folder and the contents before proceeding. 2. Enter the following commands. + ```bash + python create_wds_shards.py ${COMMON_VOICE_PATH}/processed_data/train ${COMMON_VOICE_PATH}/processed_data/commonVoice_shards/train + python create_wds_shards.py ${COMMON_VOICE_PATH}/processed_data/dev ${COMMON_VOICE_PATH}/processed_data/commonVoice_shards/dev ``` - python create_wds_shards.py /data/commonVoice/train/ /data/commonVoice_shards/train - python create_wds_shards.py /data/commonVoice/dev/ /data/commonVoice_shards/dev - ``` -3. Note the shard with the largest number as `LARGEST_SHARD_NUMBER` in the output above or by navigating to `/data/commonVoice_shards/train`. +3. Note the shard with the largest number as `LARGEST_SHARD_NUMBER` in the output above or by navigating to `${COMMON_VOICE_PATH}/processed_data/commonVoice_shards/train`. 4. Open the `train_ecapa.yaml` file and modify the `train_shards` variable to make the range reflect: `000000..LARGEST_SHARD_NUMBER`. -5. Repeat the process for `/data/commonVoice_shards/dev`. +5. Repeat Steps 3 and 4 for `${COMMON_VOICE_PATH}/processed_data/commonVoice_shards/dev`. #### Run the Training Script -The YAML file `train_ecapa.yaml` with the training configurations should already be patched from the Prerequisite section. +The YAML file `train_ecapa.yaml` with the training configurations is passed as an argument to the `train.py` script to train the model. 1. If necessary, edit the `train_ecapa.yaml` file to meet your needs. | Parameters | Description |:--- |:--- + | `seed` | The seed value, which should be set to a different value for subsequent runs. Defaults to 1987. | `out_n_neurons` | Must be equal to the number of languages of interest. | `number_of_epochs` | Default is **10**. Adjust as needed. - | `batch_size` | In the trainloader_options, decrease this value if your CPU or GPU runs out of memory while running the training script. + | `batch_size` | In the trainloader_options, decrease this value if your CPU or GPU runs out of memory while running the training script. If you see a "Killed" error message, then the training script has run out of memory. 2. Run the script to train the model. ``` @@ -195,30 +195,48 @@ The YAML file `train_ecapa.yaml` with the training configurations should already #### Move Model to Inference Folder -After training, the output should be inside `results/epaca/SEED_VALUE` folder. By default SEED_VALUE is set to 1987 in the YAML file. You can change the value as needed. +After training, the output should be inside the `results/epaca/1987` folder. By default the `seed` is set to 1987 in `train_ecapa.yaml`. You can change the value as needed. -1. Copy all files with *cp -R* from `results/epaca/SEED_VALUE` into a new folder called `lang_id_commonvoice_model` in the **Inference** folder. - - The name of the folder MUST match with the pretrained_path variable defined in the YAML file. By default, it is `lang_id_commonvoice_model`. +1. Copy all files from `results/epaca/1987` into a new folder called `lang_id_commonvoice_model` in the **Inference** folder. + ```bash + cp -R results/epaca/1987 ../Inference/lang_id_commonvoice_model + ``` + The name of the folder MUST match with the pretrained_path variable defined in `train_ecapa.yaml`. By default, it is `lang_id_commonvoice_model`. 2. Change directory to `/Inference/lang_id_commonvoice_model/save`. + ```bash + cd ../Inference/lang_id_commonvoice_model/save + ``` + 3. Copy the `label_encoder.txt` file up one level. -4. Change to the latest `CKPT` folder, and copy the classifier.ckpt and embedding_model.ckpt files into the `/Inference/lang_id_commonvoice_model/` folder. + ```bash + cp label_encoder.txt ../. + ``` + +4. Change to the latest `CKPT` folder, and copy the classifier.ckpt and embedding_model.ckpt files into the `/Inference/lang_id_commonvoice_model/` folder which is two directories up. By default, the command below will navigate into the single CKPT folder that is present, but you can change it to the specific folder name. + ```bash + # Navigate into the CKPT folder + cd CKPT* + + cp classifier.ckpt ../../. + cp embedding_model.ckpt ../../ + cd ../../../.. + ``` - You may need to modify the permissions of these files to be executable before you run the inference scripts to consume them. + You may need to modify the permissions of these files to be executable i.e. `sudo chmod 755` before you run the inference scripts to consume them. >**Note**: If `train.py` is rerun with the same seed, it will resume from the epoch number it last run. For a clean rerun, delete the `results` folder or change the seed. You can now load the model for inference. In the `Inference` folder, the `inference_commonVoice.py` script uses the trained model on the testing dataset, whereas `inference_custom.py` uses the trained model on a user-specified dataset and can utilize Voice Activity Detection. ->**Note**: If the folder name containing the model is changed from `lang_id_commonvoice_model`, you will need to modify the `source_model_path` variable in `inference_commonVoice.py` and `inference_custom.py` files in the `speechbrain_inference` class. +>**Note**: If the folder name containing the model is changed from `lang_id_commonvoice_model`, you will need to modify the `pretrained_path` in `train_ecapa.yaml`, and the `source_model_path` variable in both the `inference_commonVoice.py` and `inference_custom.py` files in the `speechbrain_inference` class. ## Run Inference for Language Identification >**Stop**: If you have not already done so, you must run the scripts in the `Training` folder to generate the trained model before proceeding. -To run inference, you must have already run all of the training scripts, generated the trained model, and moved files to the appropriate locations. You must place the model output in a folder name matching the name specified as the `pretrained_path` variable defined in the YAML file. +To run inference, you must have already run all of the training scripts, generated the trained model, and moved files to the appropriate locations. You must place the model output in a folder name matching the name specified as the `pretrained_path` variable defined in `train_ecapa.yaml`. >**Note**: If you plan to run inference on **custom data**, you will need to create a folder for the **.wav** files to be used for prediction. For example, `data_custom`. Move the **.wav** files to your custom folder. (For quick results, you may select a few audio files from each language downloaded from CommonVoice.) @@ -226,35 +244,23 @@ To run inference, you must have already run all of the training scripts, generat 1. Change to the `Inference` directory. ``` - cd /Inference - ``` -2. Source the bash script to install or update the necessary components. - ``` - source initialize.sh - ``` -3. Patch the Intel® Extension for PyTorch (IPEX) to use SpeechBrain models. (This patch is required for PyTorch* TorchScript to work because the output of the model must contain only tensors.) - ``` - patch ./speechbrain/speechbrain/pretrained/interfaces.py < interfaces.patch + cd ./Inference ``` -### Run in Jupyter Notebook +### Option 1: Run in Jupyter Notebook -1. If you have not already done so, install Jupyter Notebook. - ``` - pip install notebook - ``` -2. Launch Jupyter Notebook. +1. Launch Jupyter Notebook. ``` - jupyter notebook --ip 0.0.0.0 --port 8888 --allow-root + jupyter notebook --ip 0.0.0.0 --port 8889 --allow-root ``` -3. Follow the instructions to open the URL with the token in your browser. -4. Locate and select the inference Notebook. +2. Follow the instructions to open the URL with the token in your browser. +3. Locate and select the inference Notebook. ``` lang_id_inference.ipynb ``` -5. Follow the instructions in the Notebook. +4. Follow the instructions in the Notebook. -### Run in a Console +### Option 2: Run in a Console If you cannot or do not want to use Jupyter Notebook, use these procedures to run the sample and scripts locally. @@ -287,34 +293,32 @@ Both scripts support input options; however, some options can be use on `inferen #### On the CommonVoice Dataset 1. Run the inference_commonvoice.py script. - ``` - python inference_commonVoice.py -p /data/commonVoice/test + ```bash + python inference_commonVoice.py -p ${COMMON_VOICE_PATH}/processed_data/test ``` The script should create a `test_data_accuracy.csv` file that summarizes the results. #### On Custom Data -1. Modify the `audio_ground_truth_labels.csv` file to include the name of the audio file and expected audio label (like, `en` for English). +To run inference on custom data, you must specify a folder with **.wav** files and pass the path in as an argument. You can do so by creating a folder named `data_custom` and then copy 1 or 2 **.wav** files from your test dataset into it. **.mp3** files will NOT work. - By default, this is disabled. If required, use the `--ground_truth_compare` input option. To run inference on custom data, you must specify a folder with **.wav** files and pass the path in as an argument. - -2. Run the inference_ script. - ``` - python inference_custom.py -p - ``` +Run the inference_ script. +```bash +python inference_custom.py -p +``` The following examples describe how to use the scripts to produce specific outcomes. **Default: Random Selections** 1. To randomly select audio clips from audio files for prediction, enter commands similar to the following: - ``` + ```bash python inference_custom.py -p data_custom -d 3 -s 50 ``` This picks 50 3-second samples from each **.wav** file in the `data_custom` folder. The `output_summary.csv` file summarizes the results. 2. To randomly select audio clips from audio files after applying **Voice Activity Detection (VAD)**, use the `--vad` option: - ``` + ```bash python inference_custom.py -p data_custom -d 3 -s 50 --vad ``` Again, the `output_summary.csv` file summarizes the results. @@ -324,18 +328,20 @@ The following examples describe how to use the scripts to produce specific outco **Optimization with Intel® Extension for PyTorch (IPEX)** 1. To optimize user-defined data, enter commands similar to the following: - ``` + ```bash python inference_custom.py -p data_custom -d 3 -s 50 --vad --ipex --verbose ``` + This will apply `ipex.optimize` to the model(s) and TorchScript. You can also add the `--bf16` option along with `--ipex` to run in the BF16 data type, supported on 4th Gen Intel® Xeon® Scalable processors and newer. + >**Note**: The `--verbose` option is required to view the latency measurements. **Quantization with Intel® Neural Compressor (INC)** 1. To improve inference latency, you can use the Intel® Neural Compressor (INC) to quantize the trained model from FP32 to INT8 by running `quantize_model.py`. + ```bash + python quantize_model.py -p ./lang_id_commonvoice_model -datapath $COMMON_VOICE_PATH/processed_data/dev ``` - python quantize_model.py -p ./lang_id_commonvoice_model -datapath $COMMON_VOICE_PATH/dev - ``` - Use the `-datapath` argument to specify a custom evaluation dataset. By default, the datapath is set to the `/data/commonVoice/dev` folder that was generated from the data preprocessing scripts in the `Training` folder. + Use the `-datapath` argument to specify a custom evaluation dataset. By default, the datapath is set to the `$COMMON_VOICE_PATH/processed_data/dev` folder that was generated from the data preprocessing scripts in the `Training` folder. After quantization, the model will be stored in `lang_id_commonvoice_model_INT8` and `neural_compressor.utils.pytorch.load` will have to be used to load the quantized model for inference. If `self.language_id` is the original model and `data_path` is the path to the audio file: ``` @@ -345,9 +351,16 @@ The following examples describe how to use the scripts to produce specific outco prediction = self.model_int8(signal) ``` -### Troubleshooting + The code above is integrated into `inference_custom.py`. You can now run inference on your data using this INT8 model: + ```bash + python inference_custom.py -p data_custom -d 3 -s 50 --vad --int8_model --verbose + ``` + + >**Note**: The `--verbose` option is required to view the latency measurements. + +**(Optional) Comparing Predictions with Ground Truth** -If the model appears to be giving the same output regardless of input, try running `clean.sh` to remove the `RIR_NOISES` and `speechbrain` folders. Redownload that data after cleaning by running `initialize.sh` and either `inference_commonVoice.py` or `inference_custom.py`. +You can choose to modify `audio_ground_truth_labels.csv` to include the name of the audio file and expected audio label (like, `en` for English), then run `inference_custom.py` with the `--ground_truth_compare` option. By default, this is disabled. ## License diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/clean.sh b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/clean.sh index f60b245773..30f1806c10 100644 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/clean.sh +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/clean.sh @@ -1,5 +1,4 @@ #!/bin/bash -rm -R RIRS_NOISES -rm -R speechbrain -rm -f rirs_noises.zip noise.csv reverb.csv +echo "Deleting rir, noise, speechbrain" +rm -R rir noise diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/create_wds_shards.patch b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/create_wds_shards.patch index ddfe37588b..3d60bc627f 100644 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/create_wds_shards.patch +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/create_wds_shards.patch @@ -1,5 +1,5 @@ ---- create_wds_shards.py 2022-09-20 14:55:48.732386718 -0700 -+++ create_wds_shards_commonvoice.py 2022-09-20 14:53:56.554637629 -0700 +--- create_wds_shards.py 2024-11-13 18:08:07.440000000 -0800 ++++ create_wds_shards_modified.py 2024-11-14 14:09:36.225000000 -0800 @@ -27,7 +27,10 @@ t, sr = torchaudio.load(audio_file_path) @@ -12,7 +12,7 @@ return t -@@ -61,27 +64,20 @@ +@@ -66,27 +69,22 @@ sample_keys_per_language = defaultdict(list) for f in audio_files: @@ -23,7 +23,9 @@ - f.as_posix(), - ) + # Common Voice format -+ # commonVoice_folder_path/common_voice__00000000.wav' ++ # commonVoice_folder_path/processed_data//common_voice__00000000.wav' ++ # DATASET_TYPE: dev, test, train ++ # LANG_ID: the label for the language + m = re.match(r"((.*)(common_voice_)(.+)(_)(\d+).wav)", f.as_posix()) + if m: diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/initialize.sh b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/initialize.sh deleted file mode 100644 index 78c114f2dc..0000000000 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/initialize.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -# Activate the oneAPI environment for PyTorch -source activate pytorch - -# Install speechbrain -git clone https://github.com/speechbrain/speechbrain.git -cd speechbrain -pip install -r requirements.txt -pip install --editable . -cd .. - -# Add speechbrain to environment variable PYTHONPATH -export PYTHONPATH=$PYTHONPATH:/Training/speechbrain - -# Install webdataset -pip install webdataset==0.1.96 - -# Install PyTorch and Intel Extension for PyTorch (IPEX) -pip install torch==1.13.1 torchaudio -pip install --no-deps torchvision==0.14.0 -pip install intel_extension_for_pytorch==1.13.100 - -# Install libraries for MP3 to WAV conversion -pip install pydub -apt-get update && apt-get install ffmpeg diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/lang_id_training.ipynb b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/lang_id_training.ipynb index 0502d223e9..4550b88916 100644 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/lang_id_training.ipynb +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/lang_id_training.ipynb @@ -29,9 +29,9 @@ "metadata": {}, "outputs": [], "source": [ - "!cp speechbrain/recipes/VoxLingua107/lang_id/create_wds_shards.py create_wds_shards.py\n", - "!cp speechbrain/recipes/VoxLingua107/lang_id/train.py train.py\n", - "!cp speechbrain/recipes/VoxLingua107/lang_id/hparams/train_ecapa.yaml train_ecapa.yaml" + "!cp ../speechbrain/recipes/VoxLingua107/lang_id/create_wds_shards.py create_wds_shards.py\n", + "!cp ../speechbrain/recipes/VoxLingua107/lang_id/train.py train.py\n", + "!cp ../speechbrain/recipes/VoxLingua107/lang_id/hparams/train_ecapa.yaml train_ecapa.yaml" ] }, { @@ -75,7 +75,7 @@ "metadata": {}, "outputs": [], "source": [ - "!python prepareAllCommonVoice.py -path /data -max_samples 2000 --createCsv --train --dev --test" + "!python prepareAllCommonVoice.py -path $COMMON_VOICE_PATH -max_samples 2000 --createCsv --train --dev --test" ] }, { @@ -102,15 +102,15 @@ "metadata": {}, "outputs": [], "source": [ - "!python create_wds_shards.py /data/commonVoice/train/ /data/commonVoice_shards/train \n", - "!python create_wds_shards.py /data/commonVoice/dev/ /data/commonVoice_shards/dev" + "!python create_wds_shards.py ${COMMON_VOICE_PATH}/processed_data/train ${COMMON_VOICE_PATH}/processed_data/commonVoice_shards/train \n", + "!python create_wds_shards.py ${COMMON_VOICE_PATH}/processed_data/dev ${COMMON_VOICE_PATH}/processed_data/commonVoice_shards/dev" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Note down the shard with the largest number as LARGEST_SHARD_NUMBER in the output above or by navigating to */data/commonVoice_shards/train*. In *train_ecapa.yaml*, modify the *train_shards* variable to go from 000000..LARGEST_SHARD_NUMBER. Repeat the process for */data/commonVoice_shards/dev*. " + "Note down the shard with the largest number as LARGEST_SHARD_NUMBER in the output above or by navigating to `${COMMON_VOICE_PATH}/processed_data/commonVoice_shards/train`. In `train_ecapa.yaml`, modify the `train_shards` variable to go from 000000..LARGEST_SHARD_NUMBER. Repeat the process for `${COMMON_VOICE_PATH}/processed_data/commonVoice_shards/dev`. " ] }, { @@ -126,6 +126,7 @@ "source": [ "### Run the training script \n", "The YAML file *train_ecapa.yaml* with the training configurations should already be patched from the Prerequisite section. The following parameters can be adjusted in the file directly as needed: \n", + "* *seed* should be set to a different value for subsequent runs. Defaults to 1987\n", "* *out_n_neurons* must be equal to the number of languages of interest \n", "* *number_of_epochs* is set to 10 by default but can be adjusted \n", "* In the trainloader_options, the *batch_size* may need to be decreased if your CPU or GPU runs out of memory while running the training script. \n", @@ -147,18 +148,57 @@ "metadata": {}, "source": [ "### Move output model to Inference folder \n", - "After training, the output should be inside results/epaca/SEED_VALUE. By default SEED_VALUE is set to 1987 in the YAML file. This value can be changed. Follow these instructions next: \n", + "After training, the output should be inside the `results/epaca/1987` folder. By default the `seed` is set to 1987 in `train_ecapa.yaml`. You can change the value as needed.\n", "\n", - "1. Copy all files with *cp -R* from results/epaca/SEED_VALUE into a new folder called *lang_id_commonvoice_model* in the Inference folder. The name of the folder MUST match with the pretrained_path variable defined in the YAML file. By default, it is *lang_id_commonvoice_model*. \n", - "2. Navigate to /Inference/land_id_commonvoice_model/save. \n", - "3. Copy the label_encoder.txt file up one level. \n", - "4. Navigate into the latest CKPT folder and copy the classifier.ckpt and embedding_model.ckpt files into the /Inference/lang_id_commonvoice_model/ level. You may need to modify the permissions of these files to be executable before you run the inference scripts to consume them. \n", + "1. Copy all files from `results/epaca/1987` into a new folder called `lang_id_commonvoice_model` in the **Inference** folder.\n", + " The name of the folder MUST match with the pretrained_path variable defined in `train_ecapa.yaml`. By default, it is `lang_id_commonvoice_model`.\n", "\n", - "Note that if *train.py* is rerun with the same seed, it will resume from the epoch number it left off of. For a clean rerun, delete the *results* folder or change the seed. \n", + "2. Change directory to `/Inference/lang_id_commonvoice_model/save`.\n", "\n", - "### Running inference\n", - "At this point, the model can be loaded and used in inference. In the Inference folder, inference_commonVoice.py uses the trained model on \n", - "the testing dataset, whereas inference_custom.py uses the trained model on a user-specified dataset and utilizes Voice Activity Detection. Note that if the folder name containing the model is changed from *lang_id_commonvoice_model*, you will need to modify inference_commonVoice.py and inference_custom.py's *source_model_path* variable in the *speechbrain_inference* class. " + "3. Copy the `label_encoder.txt` file up one level.\n", + "\n", + "4. Change to the latest `CKPT` folder, and copy the classifier.ckpt and embedding_model.ckpt files into the `/Inference/lang_id_commonvoice_model/` folder which is two directories up." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "# 1)\n", + "!cp -R results/epaca/1987 ../Inference/lang_id_commonvoice_model\n", + "\n", + "# 2)\n", + "os.chdir(\"../Inference/lang_id_commonvoice_model/save\")\n", + "\n", + "# 3)\n", + "!cp label_encoder.txt ../.\n", + "\n", + "# 4) \n", + "folders = os.listdir()\n", + "for folder in folders:\n", + " if \"CKPT\" in folder:\n", + " os.chdir(folder)\n", + " break\n", + "!cp classifier.ckpt ../../.\n", + "!cp embedding_model.ckpt ../../\n", + "os.chdir(\"../../../..\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You may need to modify the permissions of these files to be executable i.e. `sudo chmod 755` before you run the inference scripts to consume them.\n", + "\n", + ">**Note**: If `train.py` is rerun with the same seed, it will resume from the epoch number it last run. For a clean rerun, delete the `results` folder or change the seed.\n", + "\n", + "You can now load the model for inference. In the `Inference` folder, the `inference_commonVoice.py` script uses the trained model on the testing dataset, whereas `inference_custom.py` uses the trained model on a user-specified dataset and can utilize Voice Activity Detection. \n", + "\n", + ">**Note**: If the folder name containing the model is changed from `lang_id_commonvoice_model`, you will need to modify the `pretrained_path` in `train_ecapa.yaml`, and the `source_model_path` variable in both the `inference_commonVoice.py` and `inference_custom.py` files in the `speechbrain_inference` class. " ] } ], diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/prepareAllCommonVoice.py b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/prepareAllCommonVoice.py index ed78ab5c35..a6ab8df1b2 100644 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/prepareAllCommonVoice.py +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/prepareAllCommonVoice.py @@ -124,9 +124,9 @@ def main(argv): createCsv = args.createCsv # Data paths - TRAIN_PATH = commonVoicePath + "/commonVoice/train" - TEST_PATH = commonVoicePath + "/commonVoice/test" - DEV_PATH = commonVoicePath + "/commonVoice/dev" + TRAIN_PATH = commonVoicePath + "/processed_data/train" + TEST_PATH = commonVoicePath + "/processed_data/test" + DEV_PATH = commonVoicePath + "/processed_data/dev" # Prepare the csv files for the Common Voice dataset if createCsv: diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/train_ecapa.patch b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/train_ecapa.patch index 38db22cf39..c95bf540ad 100644 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/train_ecapa.patch +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/train_ecapa.patch @@ -1,60 +1,55 @@ ---- train_ecapa.yaml.orig 2023-02-09 17:17:34.849537612 +0000 -+++ train_ecapa.yaml 2023-02-09 17:19:42.936542193 +0000 -@@ -4,19 +4,19 @@ +--- train_ecapa.yaml 2024-11-13 18:08:40.313000000 -0800 ++++ train_ecapa_modified.yaml 2024-11-14 14:52:31.232000000 -0800 +@@ -4,17 +4,17 @@ # ################################ # Basic parameters -seed: 1988 +seed: 1987 - __set_seed: !apply:torch.manual_seed [!ref ] + __set_seed: !apply:speechbrain.utils.seed_everything [!ref ] output_folder: !ref results/epaca/ save_folder: !ref /save train_log: !ref /train_log.txt -data_folder: !PLACEHOLDER +data_folder: ./ - rir_folder: !ref - # skip_prep: False -shards_url: /data/voxlingua107_shards -+shards_url: /data/commonVoice_shards ++shards_url: /data/commonVoice/processed_data/commonVoice_shards train_meta: !ref /train/meta.json val_meta: !ref /dev/meta.json -train_shards: !ref /train/shard-{000000..000507}.tar +train_shards: !ref /train/shard-{000000..000000}.tar val_shards: !ref /dev/shard-000000.tar - # Set to directory on a large disk if you are training on Webdataset shards hosted on the web -@@ -25,7 +25,7 @@ + # Data for augmentation +@@ -32,7 +32,7 @@ ckpt_interval_minutes: 5 # Training parameters -number_of_epochs: 40 -+number_of_epochs: 10 ++number_of_epochs: 3 lr: 0.001 lr_final: 0.0001 sample_rate: 16000 -@@ -38,11 +38,11 @@ +@@ -45,10 +45,10 @@ deltas: False # Number of languages -out_n_neurons: 107 +out_n_neurons: 2 +-num_workers: 4 +-batch_size: 128 ++num_workers: 1 ++batch_size: 64 + batch_size_val: 32 train_dataloader_options: -- num_workers: 4 -- batch_size: 128 -+ num_workers: 1 -+ batch_size: 64 + num_workers: !ref +@@ -60,6 +60,21 @@ - val_dataloader_options: - num_workers: 1 -@@ -138,3 +138,20 @@ - classifier: !ref - normalizer: !ref - counter: !ref -+ -+# Below most relevant for inference using self-trained model: -+ + ############################## Augmentations ################################### + ++# Changes for code sample to work with CommonVoice dataset +pretrained_path: lang_id_commonvoice_model + +label_encoder: !new:speechbrain.dataio.encoder.CategoricalEncoder @@ -69,3 +64,6 @@ + classifier: !ref /classifier.ckpt + label_encoder: !ref /label_encoder.txt + + # Download and prepare the dataset of noisy sequences for augmentation + prepare_noise_data: !name:speechbrain.augment.preparation.prepare_dataset_from_URL + URL: !ref diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/initialize.sh b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/initialize.sh new file mode 100644 index 0000000000..0021b588b1 --- /dev/null +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/initialize.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Install huggingface datasets and other requirements +conda install -c conda-forge -y datasets tqdm librosa jupyter ipykernel ipywidgets + +# Install speechbrain +git clone --depth 1 --branch v1.0.2 https://github.com/speechbrain/speechbrain.git +cd speechbrain +python -m pip install -r requirements.txt +python -m pip install --editable . +cd .. + +# Add speechbrain to environment variable PYTHONPATH +export PYTHONPATH=$PYTHONPATH:$(pwd)/speechbrain + +# Install webdataset +python -m pip install webdataset==0.2.100 + +# Install libraries for MP3 to WAV conversion +python -m pip install pydub + +# Install notebook to run Jupyter notebooks +python -m pip install notebook diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/launch_docker.sh b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/launch_docker.sh deleted file mode 100644 index 546523f6f6..0000000000 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/launch_docker.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -export COMMON_VOICE_PATH="/data/commonVoice" -export DOCKER_RUN_ENVS="-e ftp_proxy=${ftp_proxy} -e FTP_PROXY=${FTP_PROXY} -e http_proxy=${http_proxy} -e HTTP_PROXY=${HTTP_PROXY} -e https_proxy=${https_proxy} -e HTTPS_PROXY=${HTTPS_PROXY} -e no_proxy=${no_proxy} -e NO_PROXY=${NO_PROXY} -e socks_proxy=${socks_proxy} -e SOCKS_PROXY=${SOCKS_PROXY} -e COMMON_VOICE_PATH=${COMMON_VOICE_PATH}" -docker run --privileged ${DOCKER_RUN_ENVS} -it --rm --network host \ - -v"/home:/home" \ - -v"/tmp:/tmp" \ - -v "${PWD}/Inference":/Inference \ - -v "${PWD}/Training":/Training \ - -v "${COMMON_VOICE_PATH}":/data \ - --shm-size 32G \ - intel/oneapi-aikit - \ No newline at end of file diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/sample.json b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/sample.json index ba157302ff..768ed8eb6d 100644 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/sample.json +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/sample.json @@ -12,8 +12,19 @@ { "id": "Language_Identification_E2E", "env": [ + "export COMMON_VOICE_PATH=/data/commonVoice" ], "steps": [ + "mkdir -p /data/commonVoice", + "apt-get update && apt-get install ffmpeg libgl1 -y", + "source initialize.sh", + "cd ./Dataset", + "python get_dataset.py --output_dir ${COMMON_VOICE_PATH}", + "cd ..", + "cd ./Training", + "jupyter nbconvert --execute --to notebook --inplace --debug lang_id_training.ipynb", + "cd ./Inference", + "jupyter nbconvert --execute --to notebook --inplace --debug lang_id_inference.ipynb" ] } ] From d7a42702c28a6a6f8f80d3031ccd5b61ee1f80ee Mon Sep 17 00:00:00 2001 From: Jimmy Wei Date: Mon, 16 Dec 2024 03:42:28 -0800 Subject: [PATCH 3/7] 2025.0 AI Tools Release - DO NOT MERGE YET (#2554) * Updated 2025.0 AI Tools release * adding lang ID modernization changes --------- Co-authored-by: alexsin368 --- .../JobRecommendationSystem.ipynb | 1386 +++++++++++++++++ .../JobRecommendationSystem.py | 634 ++++++++ .../JobRecommendationSystem/License.txt | 7 + .../JobRecommendationSystem/README.md | 177 +++ .../JobRecommendationSystem/requirements.txt | 10 + .../JobRecommendationSystem/sample.json | 29 + .../third-party-programs.txt | 253 +++ .../Dataset/get_dataset.py | 35 + .../LanguageIdentification/Inference/clean.sh | 6 +- .../Inference/inference_commonVoice.py | 10 +- .../Inference/inference_custom.py | 67 +- .../Inference/initialize.sh | 23 - .../Inference/interfaces.patch | 11 - .../Inference/lang_id_inference.ipynb | 46 +- .../Inference/quantize_model.py | 4 +- .../Inference/sample_input_features.pt | Bin 48939 -> 0 bytes .../Inference/sample_wavs.pt | Bin 0 -> 129200 bytes .../LanguageIdentification/README.md | 259 +-- .../LanguageIdentification/Training/clean.sh | 5 +- .../Training/create_wds_shards.patch | 10 +- .../Training/initialize.sh | 26 - .../Training/lang_id_training.ipynb | 72 +- .../Training/prepareAllCommonVoice.py | 6 +- .../Training/train_ecapa.patch | 46 +- .../LanguageIdentification/initialize.sh | 23 + .../LanguageIdentification/launch_docker.sh | 13 - .../LanguageIdentification/sample.json | 11 + .../IntelJAX_GettingStarted/.gitkeep | 0 .../IntelJAX_GettingStarted/License.txt | 7 + .../IntelJAX_GettingStarted/README.md | 140 ++ .../IntelJAX_GettingStarted/run.sh | 6 + .../IntelJAX_GettingStarted/sample.json | 24 + .../third-party-programs.txt | 253 +++ .../Getting-Started-Samples/README.md | 1 + 34 files changed, 3312 insertions(+), 288 deletions(-) create mode 100644 AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/JobRecommendationSystem.ipynb create mode 100644 AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/JobRecommendationSystem.py create mode 100644 AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/License.txt create mode 100644 AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/README.md create mode 100644 AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/requirements.txt create mode 100644 AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/sample.json create mode 100644 AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/third-party-programs.txt create mode 100644 AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Dataset/get_dataset.py delete mode 100644 AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/initialize.sh delete mode 100644 AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/interfaces.patch delete mode 100644 AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/sample_input_features.pt create mode 100644 AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/sample_wavs.pt delete mode 100644 AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/initialize.sh create mode 100644 AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/initialize.sh delete mode 100644 AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/launch_docker.sh create mode 100644 AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/.gitkeep create mode 100644 AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/License.txt create mode 100644 AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/README.md create mode 100644 AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/run.sh create mode 100644 AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/sample.json create mode 100644 AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/third-party-programs.txt diff --git a/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/JobRecommendationSystem.ipynb b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/JobRecommendationSystem.ipynb new file mode 100644 index 0000000000..2446f11fc8 --- /dev/null +++ b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/JobRecommendationSystem.ipynb @@ -0,0 +1,1386 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "muD_5mFUYB-x" + }, + "source": [ + "# Job recommendation system\n", + "\n", + "The code sample contains the following parts:\n", + "\n", + "1. Data exploration and visualization\n", + "2. Data cleaning/pre-processing\n", + "3. Fake job postings identification and removal\n", + "4. Job recommendation by showing the most similar job postings\n", + "\n", + "The scenario is that someone wants to find the best posting for themselves. They have collected the data, but he is not sure if all the data is real. Therefore, based on a trained model, as in this sample, they identify with a high degree of accuracy which postings are real, and it is among them that they choose the best ad for themselves.\n", + "\n", + "For simplicity, only one dataset will be used within this code, but the process using one dataset is not significantly different from the one described earlier.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GTu2WLQmZU-L" + }, + "source": [ + "## Data exploration and visualization\n", + "\n", + "For the purpose of this code sample we will use Real or Fake: Fake Job Postings dataset available over HuggingFace API. In this first part we will focus on data exploration and visualization. In standard end-to-end workload it is the first step. Engineer needs to first know the data to be able to work on it and prepare solution that will utilize dataset the best.\n", + "\n", + "Lest start with loading the dataset. We are using datasets library to do that." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "saMOoStVs0-s", + "outputId": "ba4623b9-0533-4062-b6b0-01e96bd4de39" + }, + "outputs": [], + "source": [ + "from datasets import load_dataset\n", + "\n", + "dataset = load_dataset(\"victor/real-or-fake-fake-jobposting-prediction\")\n", + "dataset = dataset['train']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To better analyze and understand the data we are transferring it to pandas DataFrame, so we are able to take benefit from all pandas data transformations. Pandas library provides multiple useful functions for data manipulation so it is usual choice at this stage of machine learning or deep learning project.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "rRkolJQKtAzt" + }, + "outputs": [], + "source": [ + "import pandas as pd\n", + "df = dataset.to_pandas()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's see 5 first and 5 last rows in the dataset we are working on." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 556 + }, + "id": "WYGIRBUJSl3N", + "outputId": "ccd4abaf-1b4d-4fbd-85c8-54408c4f9f8a" + }, + "outputs": [], + "source": [ + "df.tail()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, lets print a concise summary of the dataset. This way we will see all the column names, know the number of rows and types in every of the column. It is a great overview on the features of the dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "UtxA6fmaSrQ8", + "outputId": "e8a1ce15-88e8-487c-d05e-74c024aca994" + }, + "outputs": [], + "source": [ + "df.info()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "At this point it is a good idea to make sure our dataset doen't contain any data duplication that could impact the results of our future system. To do that we firs need to remove `job_id` column. It contains unique number for each job posting so even if the rest of the data is the same between 2 postings it makes it different." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 556 + }, + "id": "f4LJCdKHStca", + "outputId": "b1db61e1-a909-463b-d369-b38c2349cba6" + }, + "outputs": [], + "source": [ + "# Drop the 'job_id' column\n", + "df = df.drop(columns=['job_id'])\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And now, the actual duplicates removal. We first pring the number of duplicates that are in our dataset, than using `drop_duplicated` method we are removing them and after this operation printing the number of the duplicates. If everything works as expected after duplicates removal we should print `0` as current number of duplicates in the dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "Ow8SgJg2vJkB", + "outputId": "9a6050bf-f4bf-4b17-85a1-8d2980cd77ee" + }, + "outputs": [], + "source": [ + "# let's make sure that there are no duplicated jobs\n", + "\n", + "print(df.duplicated().sum())\n", + "df = df.drop_duplicates()\n", + "print(df.duplicated().sum())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tcpcjR8UUQCJ" + }, + "source": [ + "Now we can visualize the data from the dataset. First let's visualize data as it is all real, and later, for the purposes of the fake data detection, we will also visualize it spreading fake and real data.\n", + "\n", + "When working with text data it can be challenging to visualize it. Thankfully, there is a `wordcloud` library that shows common words in the analyzed texts. The bigger word is, more often the word is in the text. Wordclouds allow us to quickly identify the most important topic and themes in a large text dataset and also explore patterns and trends in textural data.\n", + "\n", + "In our example, we will create wordcloud for job titles, to have high-level overview of job postings we are working with." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 544 + }, + "id": "c0jsAvejvzQ5", + "outputId": "7622e54f-6814-47e1-d9c6-4ee13173b4f4" + }, + "outputs": [], + "source": [ + "from wordcloud import WordCloud # module to print word cloud\n", + "from matplotlib import pyplot as plt\n", + "import seaborn as sns\n", + "\n", + "# On the basis of Job Titles form word cloud\n", + "job_titles_text = ' '.join(df['title'])\n", + "wordcloud = WordCloud(width=800, height=400, background_color='white').generate(job_titles_text)\n", + "\n", + "# Plotting Word Cloud\n", + "plt.figure(figsize=(10, 6))\n", + "plt.imshow(wordcloud, interpolation='bilinear')\n", + "plt.title('Job Titles')\n", + "plt.axis('off')\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Different possibility to get some information from this type of dataset is by showing top-n most common values in given column or distribution of the values int his column.\n", + "Let's show top 10 most common job titles and compare this result with previously showed wordcould." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 564 + }, + "id": "0Ut0qo0ywv3_", + "outputId": "705fbbf0-4dc0-4ee1-d821-edccaff78a85" + }, + "outputs": [], + "source": [ + "# Get Count of job title\n", + "job_title_counts = df['title'].value_counts()\n", + "\n", + "# Plotting a bar chart for the top 10 most common job titles\n", + "top_job_titles = job_title_counts.head(10)\n", + "plt.figure(figsize=(10, 6))\n", + "top_job_titles.sort_values().plot(kind='barh')\n", + "plt.title('Top 10 Most Common Job Titles')\n", + "plt.xlabel('Frequency')\n", + "plt.ylabel('Job Titles')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can do the same for different columns, as `employment_type`, `required_experience`, `telecommuting`, `has_company_logo` and `has_questions`. These should give us reale good overview of different parts of our dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 564 + }, + "id": "OaBkEWNLxkqK", + "outputId": "efbd9955-5630-4fdb-a6dd-f4ffe4b0a7d8" + }, + "outputs": [], + "source": [ + "# Count the occurrences of each work type\n", + "work_type_counts = df['employment_type'].value_counts()\n", + "\n", + "# Plotting the distribution of work types\n", + "plt.figure(figsize=(8, 6))\n", + "work_type_counts.sort_values().plot(kind='barh')\n", + "plt.title('Distribution of Work Types Offered by Jobs')\n", + "plt.xlabel('Frequency')\n", + "plt.ylabel('Work Types')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 564 + }, + "id": "5uTBPGXgyZEV", + "outputId": "d6c76b5f-25ce-4730-f849-f881315ca883" + }, + "outputs": [], + "source": [ + "# Count the occurrences of required experience types\n", + "work_type_counts = df['required_experience'].value_counts()\n", + "\n", + "# Plotting the distribution of work types\n", + "plt.figure(figsize=(8, 6))\n", + "work_type_counts.sort_values().plot(kind='barh')\n", + "plt.title('Distribution of Required Experience by Jobs')\n", + "plt.xlabel('Frequency')\n", + "plt.ylabel('Required Experience')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For employment_type and required_experience we also created matrix to see if there is any corelation between those two. To visualize it we created heatmap. If you think that some of the parameters can be related, creating similar heatmap can be a good idea." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 696 + }, + "id": "nonO2cHR1I-t", + "outputId": "3101b8b2-cf0a-413b-b0aa-eb2a3a96a582" + }, + "outputs": [], + "source": [ + "from matplotlib import pyplot as plt\n", + "import seaborn as sns\n", + "import pandas as pd\n", + "\n", + "plt.subplots(figsize=(8, 8))\n", + "df_2dhist = pd.DataFrame({\n", + " x_label: grp['required_experience'].value_counts()\n", + " for x_label, grp in df.groupby('employment_type')\n", + "})\n", + "sns.heatmap(df_2dhist, cmap='viridis')\n", + "plt.xlabel('employment_type')\n", + "_ = plt.ylabel('required_experience')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 564 + }, + "id": "mXdpeQFJ1VMu", + "outputId": "eb9a893f-5087-4dad-ceca-48a1dfeb0b02" + }, + "outputs": [], + "source": [ + "# Count the occurrences of unique values in the 'telecommuting' column\n", + "telecommuting_counts = df['telecommuting'].value_counts()\n", + "\n", + "plt.figure(figsize=(8, 6))\n", + "telecommuting_counts.sort_values().plot(kind='barh')\n", + "plt.title('Counts of telecommuting vs Non-telecommuting')\n", + "plt.xlabel('count')\n", + "plt.ylabel('telecommuting')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 564 + }, + "id": "8kEu4IKcVmSV", + "outputId": "94ae873f-9178-4c63-e855-d677f135e552" + }, + "outputs": [], + "source": [ + "has_company_logo_counts = df['has_company_logo'].value_counts()\n", + "\n", + "plt.figure(figsize=(8, 6))\n", + "has_company_logo_counts.sort_values().plot(kind='barh')\n", + "plt.ylabel('has_company_logo')\n", + "plt.xlabel('Count')\n", + "plt.title('Counts of With_Logo vs Without_Logo')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 564 + }, + "id": "Esv8b51EVvxx", + "outputId": "40355e3f-fc6b-4b16-d459-922cfede2f71" + }, + "outputs": [], + "source": [ + "has_questions_counts = df['has_questions'].value_counts()\n", + "\n", + "# Plot the counts\n", + "plt.figure(figsize=(8, 6))\n", + "has_questions_counts.sort_values().plot(kind='barh')\n", + "plt.ylabel('has_questions')\n", + "plt.xlabel('Count')\n", + "plt.title('Counts Questions vs NO_Questions')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "From the job recommendations point of view the salary and location can be really important parameters to take into consideration. In given dataset we have salary ranges available so there is no need for additional data processing rather than removal of empty ranges but if the dataset you're working on has specific values, consider organizing it into appropriate ranges and only then displaying the result." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 564 + }, + "id": "6SQO5PVLy8vt", + "outputId": "f0dbdf21-af94-4e56-cd82-938b7258c26f" + }, + "outputs": [], + "source": [ + "# Splitting benefits by comma and creating a list of benefits\n", + "benefits_list = df['salary_range'].str.split(',').explode()\n", + "benefits_list = benefits_list[benefits_list != 'None']\n", + "benefits_list = benefits_list[benefits_list != '0-0']\n", + "\n", + "\n", + "# Counting the occurrences of each skill\n", + "benefits_count = benefits_list.str.strip().value_counts()\n", + "\n", + "# Plotting the top 10 most common benefits\n", + "top_benefits = benefits_count.head(10)\n", + "plt.figure(figsize=(10, 6))\n", + "top_benefits.sort_values().plot(kind='barh')\n", + "plt.title('Top 10 Salaries Range Offered by Companies')\n", + "plt.xlabel('Frequency')\n", + "plt.ylabel('Salary Range')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For the location we have both county, state and city specified, so we need to split it into individual columns, and then show top 10 counties and cities." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_242StA_UZTF" + }, + "outputs": [], + "source": [ + "# Split the 'location' column into separate columns for country, state, and city\n", + "location_split = df['location'].str.split(', ', expand=True)\n", + "df['Country'] = location_split[0]\n", + "df['State'] = location_split[1]\n", + "df['City'] = location_split[2]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 959 + }, + "id": "HS9SH6p9UaJU", + "outputId": "6562e31f-6719-448b-c290-1a9610eb50c2" + }, + "outputs": [], + "source": [ + "# Count the occurrences of unique values in the 'Country' column\n", + "Country_counts = df['Country'].value_counts()\n", + "\n", + "# Select the top 10 most frequent occurrences\n", + "top_10_Country = Country_counts.head(10)\n", + "\n", + "# Plot the top 10 most frequent occurrences as horizontal bar plot with rotated labels\n", + "plt.figure(figsize=(14, 10))\n", + "sns.barplot(y=top_10_Country.index, x=top_10_Country.values)\n", + "plt.ylabel('Country')\n", + "plt.xlabel('Count')\n", + "plt.title('Top 10 Most Frequent Country')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 959 + }, + "id": "j_cPJl8pUcWT", + "outputId": "bb87ec2d-750d-45b0-f64f-ae4b84b00544" + }, + "outputs": [], + "source": [ + "# Count the occurrences of unique values in the 'City' column\n", + "City_counts = df['City'].value_counts()\n", + "\n", + "# Select the top 10 most frequent occurrences\n", + "top_10_City = City_counts.head(10)\n", + "\n", + "# Plot the top 10 most frequent occurrences as horizontal bar plot with rotated labels\n", + "plt.figure(figsize=(14, 10))\n", + "sns.barplot(y=top_10_City.index, x=top_10_City.values)\n", + "plt.ylabel('City')\n", + "plt.xlabel('Count')\n", + "plt.title('Top 10 Most Frequent City')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-R8hkAIjVF_s" + }, + "source": [ + "### Fake job postings data visualization \n", + "\n", + "What about fraudulent class? Let see how many of the jobs in the dataset are fake. Whether there are equally true and false offers, or whether there is a significant disproportion between the two. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 651 + }, + "id": "KJ5Aq2IizZ4r", + "outputId": "e1c10006-9f5a-4321-d90a-28915e02f8c3" + }, + "outputs": [], + "source": [ + "## fake job visualization\n", + "# Count the occurrences of unique values in the 'fraudulent' column\n", + "fraudulent_counts = df['fraudulent'].value_counts()\n", + "\n", + "# Plot the counts using a rainbow color palette\n", + "plt.figure(figsize=(8, 6))\n", + "sns.barplot(x=fraudulent_counts.index, y=fraudulent_counts.values)\n", + "plt.xlabel('Fraudulent')\n", + "plt.ylabel('Count')\n", + "plt.title('Counts of Fraudulent vs Non-Fraudulent')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 564 + }, + "id": "oyeB2MFRVIWi", + "outputId": "9236f907-4c16-49f7-c14b-883d21ae6c2c" + }, + "outputs": [], + "source": [ + "plt.figure(figsize=(10, 6))\n", + "sns.countplot(data=df, x='employment_type', hue='fraudulent')\n", + "plt.title('Count of Fraudulent Cases by Employment Type')\n", + "plt.xlabel('Employment Type')\n", + "plt.ylabel('Count')\n", + "plt.legend(title='Fraudulent')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 564 + }, + "id": "ORGFxjVVVJBi", + "outputId": "084304de-5618-436a-8958-6f36abd72be7" + }, + "outputs": [], + "source": [ + "plt.figure(figsize=(10, 6))\n", + "sns.countplot(data=df, x='required_experience', hue='fraudulent')\n", + "plt.title('Count of Fraudulent Cases by Required Experience')\n", + "plt.xlabel('Required Experience')\n", + "plt.ylabel('Count')\n", + "plt.legend(title='Fraudulent')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "id": "GnRPXpBWVL7O", + "outputId": "8d347181-83d8-44ae-9c57-88d98825694d" + }, + "outputs": [], + "source": [ + "plt.figure(figsize=(30, 18))\n", + "sns.countplot(data=df, x='required_education', hue='fraudulent')\n", + "plt.title('Count of Fraudulent Cases by Required Education')\n", + "plt.xlabel('Required Education')\n", + "plt.ylabel('Count')\n", + "plt.legend(title='Fraudulent')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8qKuYrkvVPlO" + }, + "source": [ + "We can see that there is no connection between those parameters and fake job postings. This way in the future processing we can remove them." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BbOwzXmdaJTw" + }, + "source": [ + "## Data cleaning/pre-processing\n", + "\n", + "One of the really important step related to any type of data processing is data cleaning. For texts it usually includes removal of stop words, special characters, numbers or any additional noise like hyperlinks. \n", + "\n", + "In our case, to prepare data for Fake Job Postings recognition we will first, combine all relevant columns into single new record and then clean the data to work on it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "jYLwp2wSaMdi" + }, + "outputs": [], + "source": [ + "# List of columns to concatenate\n", + "columns_to_concat = ['title', 'location', 'department', 'salary_range', 'company_profile',\n", + " 'description', 'requirements', 'benefits', 'employment_type',\n", + " 'required_experience', 'required_education', 'industry', 'function']\n", + "\n", + "# Concatenate the values of specified columns into a new column 'job_posting'\n", + "df['job_posting'] = df[columns_to_concat].apply(lambda x: ' '.join(x.dropna().astype(str)), axis=1)\n", + "\n", + "# Create a new DataFrame with columns 'job_posting' and 'fraudulent'\n", + "new_df = df[['job_posting', 'fraudulent']].copy()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 206 + }, + "id": "FulR3zMiaMgI", + "outputId": "995058f3-f5f7-4aec-e1e0-94d42aad468f" + }, + "outputs": [], + "source": [ + "new_df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "0TpoEx1-YgCs", + "outputId": "8eaaf021-ae66-477a-f07b-fd8a353d17eb", + "scrolled": true + }, + "outputs": [], + "source": [ + "# import spacy\n", + "import re\n", + "import nltk\n", + "from nltk.corpus import stopwords\n", + "\n", + "nltk.download('stopwords')\n", + "\n", + "def preprocess_text(text):\n", + " # Remove newlines, carriage returns, and tabs\n", + " text = re.sub('\\n','', text)\n", + " text = re.sub('\\r','', text)\n", + " text = re.sub('\\t','', text)\n", + " # Remove URLs\n", + " text = re.sub(r\"http\\S+|www\\S+|https\\S+\", \"\", text, flags=re.MULTILINE)\n", + "\n", + " # Remove special characters\n", + " text = re.sub(r\"[^a-zA-Z0-9\\s]\", \"\", text)\n", + "\n", + " # Remove punctuation\n", + " text = re.sub(r'[^\\w\\s]', '', text)\n", + "\n", + " # Remove digits\n", + " text = re.sub(r'\\d', '', text)\n", + "\n", + " # Convert to lowercase\n", + " text = text.lower()\n", + "\n", + " # Remove stop words\n", + " stop_words = set(stopwords.words('english'))\n", + " words = [word for word in text.split() if word.lower() not in stop_words]\n", + " text = ' '.join(words)\n", + "\n", + " return text\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "p9NHS6Vx2BE8" + }, + "outputs": [], + "source": [ + "new_df['job_posting'] = new_df['job_posting'].apply(preprocess_text)\n", + "\n", + "new_df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The next step in the pre-processing is lemmatization. It is a process to reduce a word to its root form, called a lemma. For example the verb 'planning' would be changed to 'plan' world." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "kZnHODi-ZK33" + }, + "outputs": [], + "source": [ + "# Lemmatization\n", + "import en_core_web_sm\n", + "\n", + "nlp = en_core_web_sm.load()\n", + "\n", + "def lemmatize_text(text):\n", + " doc = nlp(text)\n", + " return \" \".join([token.lemma_ for token in doc])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "uQauVQdw2LWa" + }, + "outputs": [], + "source": [ + "new_df['job_posting'] = new_df['job_posting'].apply(lemmatize_text)\n", + "\n", + "new_df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dQDR6_SpZW0B" + }, + "source": [ + "At this stage we can also visualize the data with wordcloud by having special text column. We can show it for both fake and real dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 411 + }, + "id": "fdR9GAG6ZnPh", + "outputId": "57e9b5ae-87b4-4523-d0ae-8c8fd56cd9bc" + }, + "outputs": [], + "source": [ + "from wordcloud import WordCloud\n", + "\n", + "non_fraudulent_text = ' '.join(text for text in new_df[new_df['fraudulent'] == 0]['job_posting'])\n", + "fraudulent_text = ' '.join(text for text in new_df[new_df['fraudulent'] == 1]['job_posting'])\n", + "\n", + "wordcloud_non_fraudulent = WordCloud(width=800, height=400, background_color='white').generate(non_fraudulent_text)\n", + "\n", + "wordcloud_fraudulent = WordCloud(width=800, height=400, background_color='white').generate(fraudulent_text)\n", + "\n", + "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 10))\n", + "\n", + "ax1.imshow(wordcloud_non_fraudulent, interpolation='bilinear')\n", + "ax1.axis('off')\n", + "ax1.set_title('Non-Fraudulent Job Postings')\n", + "\n", + "ax2.imshow(wordcloud_fraudulent, interpolation='bilinear')\n", + "ax2.axis('off')\n", + "ax2.set_title('Fraudulent Job Postings')\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ihtfhOr7aNMa" + }, + "source": [ + "## Fake job postings identification and removal\n", + "\n", + "Nowadays, it is unfortunate that not all the job offers that are posted on papular portals are genuine. Some of them are created only to collect personal data. Therefore, just detecting fake job postings can be very essential. \n", + "\n", + "We will create bidirectional LSTM model with one hot encoding. Let's start with all necessary imports." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "VNdX-xcjtVS2" + }, + "outputs": [], + "source": [ + "from tensorflow.keras.layers import Embedding\n", + "from tensorflow.keras.preprocessing.sequence import pad_sequences\n", + "from tensorflow.keras.models import Sequential\n", + "from tensorflow.keras.preprocessing.text import one_hot\n", + "from tensorflow.keras.layers import Dense\n", + "from tensorflow.keras.layers import Bidirectional\n", + "from tensorflow.keras.layers import Dropout" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Make sure, you're using Tensorflow version 2.15.0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 35 + }, + "id": "IxY47-s7tbjU", + "outputId": "02d68552-ff52-422b-9044-e55e35ef1236" + }, + "outputs": [], + "source": [ + "import tensorflow as tf\n", + "tf.__version__" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let us import Intel Extension for TensorFlow*. We are using Python API `itex.experimental_ops_override()`. It automatically replace some TensorFlow operators by Custom Operators under `itex.ops` namespace, as well as to be compatible with existing trained parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import intel_extension_for_tensorflow as itex\n", + "\n", + "itex.experimental_ops_override()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We need to prepare data for the model we will create. First let's assign job_postings to X and fraudulent values to y (expected value)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "U-7klPFFtZgo" + }, + "outputs": [], + "source": [ + "X = new_df['job_posting']\n", + "y = new_df['fraudulent']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "One hot encoding is a technique to represent categorical variables as numerical values. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "3FFtUrPbtbmD" + }, + "outputs": [], + "source": [ + "voc_size = 5000\n", + "onehot_repr = [one_hot(words, voc_size) for words in X]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "ygHx6LSg6ZUr", + "outputId": "5b152a4f-621b-400c-a65b-5fa19a934aa2" + }, + "outputs": [], + "source": [ + "sent_length = 40\n", + "embedded_docs = pad_sequences(onehot_repr, padding='pre', maxlen=sent_length)\n", + "print(embedded_docs)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Creating model\n", + "\n", + "We are creating Deep Neural Network using Bidirectional LSTM. The architecture is as followed:\n", + "\n", + "* Embedding layer\n", + "* Bidirectiona LSTM Layer\n", + "* Dropout layer\n", + "* Dense layer with sigmod function\n", + "\n", + "We are using Adam optimizer with binary crossentropy. We are optimism accuracy.\n", + "\n", + "If Intel® Extension for TensorFlow* backend is XPU, `tf.keras.layers.LSTM` will be replaced by `itex.ops.ItexLSTM`. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "Vnhm4huG-Mat", + "outputId": "dbc59ef1-168a-4e11-f38d-b47674dd4be6" + }, + "outputs": [], + "source": [ + "embedding_vector_features = 50\n", + "model_itex = Sequential()\n", + "model_itex.add(Embedding(voc_size, embedding_vector_features, input_length=sent_length))\n", + "model_itex.add(Bidirectional(itex.ops.ItexLSTM(100)))\n", + "model_itex.add(Dropout(0.3))\n", + "model_itex.add(Dense(1, activation='sigmoid'))\n", + "model_itex.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])\n", + "print(model_itex.summary())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1-tz3hyc-PvN" + }, + "outputs": [], + "source": [ + "X_final = np.array(embedded_docs)\n", + "y_final = np.array(y)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "POVN7X60-TnQ" + }, + "outputs": [], + "source": [ + "from sklearn.model_selection import train_test_split\n", + "X_train, X_test, y_train, y_test = train_test_split(X_final, y_final, test_size=0.25, random_state=320)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's train the model. We are using standard `model.fit()` method providing training and testing dataset. You can easily modify number of epochs in this training process but keep in mind that the model can become overtrained, so that it will have very good results on training data, but poor results on test data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "U0cGa7ei-Ufh", + "outputId": "68ce942d-ea51-458f-ac6c-ab619ab1ce74" + }, + "outputs": [], + "source": [ + "model_itex.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=1, batch_size=64)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The values returned by the model are in the range [0,1] Need to map them to integer values of 0 or 1." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "u4I8Y-R5EcDw", + "outputId": "be384d88-b27c-49c5-bebb-e9bdba986692" + }, + "outputs": [], + "source": [ + "y_pred = (model_itex.predict(X_test) > 0.5).astype(\"int32\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To demonstrate the effectiveness of our models we presented the confusion matrix and classification report available within the `scikit-learn` library." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 675 + }, + "id": "0lB3N6fxtbom", + "outputId": "97b1713d-b373-44e1-a5b2-15e41aa84016" + }, + "outputs": [], + "source": [ + "from sklearn.metrics import ConfusionMatrixDisplay, confusion_matrix, classification_report\n", + "\n", + "conf_matrix = confusion_matrix(y_test, y_pred)\n", + "print(\"Confusion matrix:\")\n", + "print(conf_matrix)\n", + "\n", + "ConfusionMatrixDisplay.from_predictions(y_test, y_pred)\n", + "\n", + "class_report = classification_report(y_test, y_pred)\n", + "print(\"Classification report:\")\n", + "print(class_report)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ioa6oZNuaPnJ" + }, + "source": [ + "## Job recommendation by showing the most similar ones" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZGReO9ziJyXm" + }, + "source": [ + "Now, as we are sure that the data we are processing is real, we can get back to the original columns and create our recommendation system.\n", + "\n", + "Also use much more simple solution for recommendations. Even, as before we used Deep Learning to check if posting is fake, we can use classical machine learning algorithms to show similar job postings.\n", + "\n", + "First, let's filter fake job postings." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 556 + }, + "id": "RsCZLWU0aMqN", + "outputId": "503c1b4e-26db-46fd-d69f-f8ee17c1519c" + }, + "outputs": [], + "source": [ + "real = df[df['fraudulent'] == 0]\n", + "real.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After that, we create a common column containing those text parameters that we want to be compared between theses and are relevant to us when making recommendations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 206 + }, + "id": "NLc-uoYeaMsy", + "outputId": "452602b0-88e2-4c9c-a6f0-5b069cc34009" + }, + "outputs": [], + "source": [ + "cols = ['title', 'description', 'requirements', 'required_experience', 'required_education', 'industry']\n", + "real = real[cols]\n", + "real.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 293 + }, + "id": "mX-xc2OetVzx", + "outputId": "e0f24240-d8dd-4f79-fda6-db15d2f4c54f" + }, + "outputs": [], + "source": [ + "real = real.fillna(value='')\n", + "real['text'] = real['description'] + real['requirements'] + real['required_experience'] + real['required_education'] + real['industry']\n", + "real.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's see the mechanism that we will use to prepare recommendations - we will use sentence similarity based on prepared `text` column in our dataset. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from sentence_transformers import SentenceTransformer\n", + "\n", + "model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's prepare a few example sentences that cover 4 topics. On these sentences it will be easier to show how the similarities between the texts work than on the whole large dataset we have." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "messages = [\n", + " # Smartphones\n", + " \"I like my phone\",\n", + " \"My phone is not good.\",\n", + " \"Your cellphone looks great.\",\n", + "\n", + " # Weather\n", + " \"Will it snow tomorrow?\",\n", + " \"Recently a lot of hurricanes have hit the US\",\n", + " \"Global warming is real\",\n", + "\n", + " # Food and health\n", + " \"An apple a day, keeps the doctors away\",\n", + " \"Eating strawberries is healthy\",\n", + " \"Is paleo better than keto?\",\n", + "\n", + " # Asking about age\n", + " \"How old are you?\",\n", + " \"what is your age?\",\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we are preparing functions to show similarities between given sentences in the for of heat map. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import seaborn as sns\n", + "\n", + "def plot_similarity(labels, features, rotation):\n", + " corr = np.inner(features, features)\n", + " sns.set(font_scale=1.2)\n", + " g = sns.heatmap(\n", + " corr,\n", + " xticklabels=labels,\n", + " yticklabels=labels,\n", + " vmin=0,\n", + " vmax=1,\n", + " cmap=\"YlOrRd\")\n", + " g.set_xticklabels(labels, rotation=rotation)\n", + " g.set_title(\"Semantic Textual Similarity\")\n", + "\n", + "def run_and_plot(messages_):\n", + " message_embeddings_ = model.encode(messages_)\n", + " plot_similarity(messages_, message_embeddings_, 90)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "run_and_plot(messages)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's move back to our job postings dataset. First, we are using sentence encoding model to be able to calculate similarities." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "encodings = []\n", + "for text in real['text']:\n", + " encodings.append(model.encode(text))\n", + "\n", + "real['encodings'] = encodings" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then, we can chose job posting we wan to calculate similarities to. In our case it is first job posting in the dataset, but you can easily change it to any other job posting, by changing value in the `index` variable." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "index = 0\n", + "corr = np.inner(encodings[index], encodings)\n", + "real['corr_to_first'] = corr" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And based on the calculated similarities, we can show top most similar job postings, by sorting them according to calculated correlation value." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "real.sort_values(by=['corr_to_first'], ascending=False).head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this code sample we created job recommendation system. First, we explored and analyzed the dataset, then we pre-process the data and create fake job postings detection model. At the end we used sentence similarities to show top 5 recommendations - the most similar job descriptions to the chosen one. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"[CODE_SAMPLE_COMPLETED_SUCCESSFULLY]\")" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Tensorflow", + "language": "python", + "name": "tensorflow" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/JobRecommendationSystem.py b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/JobRecommendationSystem.py new file mode 100644 index 0000000000..425ab1f5dd --- /dev/null +++ b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/JobRecommendationSystem.py @@ -0,0 +1,634 @@ +# %% [markdown] +# # Job recommendation system +# +# The code sample contains the following parts: +# +# 1. Data exploration and visualization +# 2. Data cleaning/pre-processing +# 3. Fake job postings identification and removal +# 4. Job recommendation by showing the most similar job postings +# +# The scenario is that someone wants to find the best posting for themselves. They have collected the data, but he is not sure if all the data is real. Therefore, based on a trained model, as in this sample, they identify with a high degree of accuracy which postings are real, and it is among them that they choose the best ad for themselves. +# +# For simplicity, only one dataset will be used within this code, but the process using one dataset is not significantly different from the one described earlier. +# + +# %% [markdown] +# ## Data exploration and visualization +# +# For the purpose of this code sample we will use Real or Fake: Fake Job Postings dataset available over HuggingFace API. In this first part we will focus on data exploration and visualization. In standard end-to-end workload it is the first step. Engineer needs to first know the data to be able to work on it and prepare solution that will utilize dataset the best. +# +# Lest start with loading the dataset. We are using datasets library to do that. + +# %% +from datasets import load_dataset + +dataset = load_dataset("victor/real-or-fake-fake-jobposting-prediction") +dataset = dataset['train'] + +# %% [markdown] +# To better analyze and understand the data we are transferring it to pandas DataFrame, so we are able to take benefit from all pandas data transformations. Pandas library provides multiple useful functions for data manipulation so it is usual choice at this stage of machine learning or deep learning project. +# + +# %% +import pandas as pd +df = dataset.to_pandas() + +# %% [markdown] +# Let's see 5 first and 5 last rows in the dataset we are working on. + +# %% +df.head() + +# %% +df.tail() + +# %% [markdown] +# Now, lets print a concise summary of the dataset. This way we will see all the column names, know the number of rows and types in every of the column. It is a great overview on the features of the dataset. + +# %% +df.info() + +# %% [markdown] +# At this point it is a good idea to make sure our dataset doen't contain any data duplication that could impact the results of our future system. To do that we firs need to remove `job_id` column. It contains unique number for each job posting so even if the rest of the data is the same between 2 postings it makes it different. + +# %% +# Drop the 'job_id' column +df = df.drop(columns=['job_id']) +df.head() + +# %% [markdown] +# And now, the actual duplicates removal. We first pring the number of duplicates that are in our dataset, than using `drop_duplicated` method we are removing them and after this operation printing the number of the duplicates. If everything works as expected after duplicates removal we should print `0` as current number of duplicates in the dataset. + +# %% +# let's make sure that there are no duplicated jobs + +print(df.duplicated().sum()) +df = df.drop_duplicates() +print(df.duplicated().sum()) + +# %% [markdown] +# Now we can visualize the data from the dataset. First let's visualize data as it is all real, and later, for the purposes of the fake data detection, we will also visualize it spreading fake and real data. +# +# When working with text data it can be challenging to visualize it. Thankfully, there is a `wordcloud` library that shows common words in the analyzed texts. The bigger word is, more often the word is in the text. Wordclouds allow us to quickly identify the most important topic and themes in a large text dataset and also explore patterns and trends in textural data. +# +# In our example, we will create wordcloud for job titles, to have high-level overview of job postings we are working with. + +# %% +from wordcloud import WordCloud # module to print word cloud +from matplotlib import pyplot as plt +import seaborn as sns + +# On the basis of Job Titles form word cloud +job_titles_text = ' '.join(df['title']) +wordcloud = WordCloud(width=800, height=400, background_color='white').generate(job_titles_text) + +# Plotting Word Cloud +plt.figure(figsize=(10, 6)) +plt.imshow(wordcloud, interpolation='bilinear') +plt.title('Job Titles') +plt.axis('off') +plt.tight_layout() +plt.show() + +# %% [markdown] +# Different possibility to get some information from this type of dataset is by showing top-n most common values in given column or distribution of the values int his column. +# Let's show top 10 most common job titles and compare this result with previously showed wordcould. + +# %% +# Get Count of job title +job_title_counts = df['title'].value_counts() + +# Plotting a bar chart for the top 10 most common job titles +top_job_titles = job_title_counts.head(10) +plt.figure(figsize=(10, 6)) +top_job_titles.sort_values().plot(kind='barh') +plt.title('Top 10 Most Common Job Titles') +plt.xlabel('Frequency') +plt.ylabel('Job Titles') +plt.show() + +# %% [markdown] +# Now we can do the same for different columns, as `employment_type`, `required_experience`, `telecommuting`, `has_company_logo` and `has_questions`. These should give us reale good overview of different parts of our dataset. + +# %% +# Count the occurrences of each work type +work_type_counts = df['employment_type'].value_counts() + +# Plotting the distribution of work types +plt.figure(figsize=(8, 6)) +work_type_counts.sort_values().plot(kind='barh') +plt.title('Distribution of Work Types Offered by Jobs') +plt.xlabel('Frequency') +plt.ylabel('Work Types') +plt.show() + +# %% +# Count the occurrences of required experience types +work_type_counts = df['required_experience'].value_counts() + +# Plotting the distribution of work types +plt.figure(figsize=(8, 6)) +work_type_counts.sort_values().plot(kind='barh') +plt.title('Distribution of Required Experience by Jobs') +plt.xlabel('Frequency') +plt.ylabel('Required Experience') +plt.show() + +# %% [markdown] +# For employment_type and required_experience we also created matrix to see if there is any corelation between those two. To visualize it we created heatmap. If you think that some of the parameters can be related, creating similar heatmap can be a good idea. + +# %% +from matplotlib import pyplot as plt +import seaborn as sns +import pandas as pd + +plt.subplots(figsize=(8, 8)) +df_2dhist = pd.DataFrame({ + x_label: grp['required_experience'].value_counts() + for x_label, grp in df.groupby('employment_type') +}) +sns.heatmap(df_2dhist, cmap='viridis') +plt.xlabel('employment_type') +_ = plt.ylabel('required_experience') + +# %% +# Count the occurrences of unique values in the 'telecommuting' column +telecommuting_counts = df['telecommuting'].value_counts() + +plt.figure(figsize=(8, 6)) +telecommuting_counts.sort_values().plot(kind='barh') +plt.title('Counts of telecommuting vs Non-telecommuting') +plt.xlabel('count') +plt.ylabel('telecommuting') +plt.show() + +# %% +has_company_logo_counts = df['has_company_logo'].value_counts() + +plt.figure(figsize=(8, 6)) +has_company_logo_counts.sort_values().plot(kind='barh') +plt.ylabel('has_company_logo') +plt.xlabel('Count') +plt.title('Counts of With_Logo vs Without_Logo') +plt.show() + +# %% +has_questions_counts = df['has_questions'].value_counts() + +# Plot the counts +plt.figure(figsize=(8, 6)) +has_questions_counts.sort_values().plot(kind='barh') +plt.ylabel('has_questions') +plt.xlabel('Count') +plt.title('Counts Questions vs NO_Questions') +plt.show() + +# %% [markdown] +# From the job recommendations point of view the salary and location can be really important parameters to take into consideration. In given dataset we have salary ranges available so there is no need for additional data processing rather than removal of empty ranges but if the dataset you're working on has specific values, consider organizing it into appropriate ranges and only then displaying the result. + +# %% +# Splitting benefits by comma and creating a list of benefits +benefits_list = df['salary_range'].str.split(',').explode() +benefits_list = benefits_list[benefits_list != 'None'] +benefits_list = benefits_list[benefits_list != '0-0'] + + +# Counting the occurrences of each skill +benefits_count = benefits_list.str.strip().value_counts() + +# Plotting the top 10 most common benefits +top_benefits = benefits_count.head(10) +plt.figure(figsize=(10, 6)) +top_benefits.sort_values().plot(kind='barh') +plt.title('Top 10 Salaries Range Offered by Companies') +plt.xlabel('Frequency') +plt.ylabel('Salary Range') +plt.show() + +# %% [markdown] +# For the location we have both county, state and city specified, so we need to split it into individual columns, and then show top 10 counties and cities. + +# %% +# Split the 'location' column into separate columns for country, state, and city +location_split = df['location'].str.split(', ', expand=True) +df['Country'] = location_split[0] +df['State'] = location_split[1] +df['City'] = location_split[2] + +# %% +# Count the occurrences of unique values in the 'Country' column +Country_counts = df['Country'].value_counts() + +# Select the top 10 most frequent occurrences +top_10_Country = Country_counts.head(10) + +# Plot the top 10 most frequent occurrences as horizontal bar plot with rotated labels +plt.figure(figsize=(14, 10)) +sns.barplot(y=top_10_Country.index, x=top_10_Country.values) +plt.ylabel('Country') +plt.xlabel('Count') +plt.title('Top 10 Most Frequent Country') +plt.show() + +# %% +# Count the occurrences of unique values in the 'City' column +City_counts = df['City'].value_counts() + +# Select the top 10 most frequent occurrences +top_10_City = City_counts.head(10) + +# Plot the top 10 most frequent occurrences as horizontal bar plot with rotated labels +plt.figure(figsize=(14, 10)) +sns.barplot(y=top_10_City.index, x=top_10_City.values) +plt.ylabel('City') +plt.xlabel('Count') +plt.title('Top 10 Most Frequent City') +plt.show() + +# %% [markdown] +# ### Fake job postings data visualization +# +# What about fraudulent class? Let see how many of the jobs in the dataset are fake. Whether there are equally true and false offers, or whether there is a significant disproportion between the two. + +# %% +## fake job visualization +# Count the occurrences of unique values in the 'fraudulent' column +fraudulent_counts = df['fraudulent'].value_counts() + +# Plot the counts using a rainbow color palette +plt.figure(figsize=(8, 6)) +sns.barplot(x=fraudulent_counts.index, y=fraudulent_counts.values) +plt.xlabel('Fraudulent') +plt.ylabel('Count') +plt.title('Counts of Fraudulent vs Non-Fraudulent') +plt.show() + +# %% +plt.figure(figsize=(10, 6)) +sns.countplot(data=df, x='employment_type', hue='fraudulent') +plt.title('Count of Fraudulent Cases by Employment Type') +plt.xlabel('Employment Type') +plt.ylabel('Count') +plt.legend(title='Fraudulent') +plt.show() + +# %% +plt.figure(figsize=(10, 6)) +sns.countplot(data=df, x='required_experience', hue='fraudulent') +plt.title('Count of Fraudulent Cases by Required Experience') +plt.xlabel('Required Experience') +plt.ylabel('Count') +plt.legend(title='Fraudulent') +plt.show() + +# %% +plt.figure(figsize=(30, 18)) +sns.countplot(data=df, x='required_education', hue='fraudulent') +plt.title('Count of Fraudulent Cases by Required Education') +plt.xlabel('Required Education') +plt.ylabel('Count') +plt.legend(title='Fraudulent') +plt.show() + +# %% [markdown] +# We can see that there is no connection between those parameters and fake job postings. This way in the future processing we can remove them. + +# %% [markdown] +# ## Data cleaning/pre-processing +# +# One of the really important step related to any type of data processing is data cleaning. For texts it usually includes removal of stop words, special characters, numbers or any additional noise like hyperlinks. +# +# In our case, to prepare data for Fake Job Postings recognition we will first, combine all relevant columns into single new record and then clean the data to work on it. + +# %% +# List of columns to concatenate +columns_to_concat = ['title', 'location', 'department', 'salary_range', 'company_profile', + 'description', 'requirements', 'benefits', 'employment_type', + 'required_experience', 'required_education', 'industry', 'function'] + +# Concatenate the values of specified columns into a new column 'job_posting' +df['job_posting'] = df[columns_to_concat].apply(lambda x: ' '.join(x.dropna().astype(str)), axis=1) + +# Create a new DataFrame with columns 'job_posting' and 'fraudulent' +new_df = df[['job_posting', 'fraudulent']].copy() + +# %% +new_df.head() + +# %% +# import spacy +import re +import nltk +from nltk.corpus import stopwords + +nltk.download('stopwords') + +def preprocess_text(text): + # Remove newlines, carriage returns, and tabs + text = re.sub('\n','', text) + text = re.sub('\r','', text) + text = re.sub('\t','', text) + # Remove URLs + text = re.sub(r"http\S+|www\S+|https\S+", "", text, flags=re.MULTILINE) + + # Remove special characters + text = re.sub(r"[^a-zA-Z0-9\s]", "", text) + + # Remove punctuation + text = re.sub(r'[^\w\s]', '', text) + + # Remove digits + text = re.sub(r'\d', '', text) + + # Convert to lowercase + text = text.lower() + + # Remove stop words + stop_words = set(stopwords.words('english')) + words = [word for word in text.split() if word.lower() not in stop_words] + text = ' '.join(words) + + return text + + + +# %% +new_df['job_posting'] = new_df['job_posting'].apply(preprocess_text) + +new_df.head() + +# %% [markdown] +# The next step in the pre-processing is lemmatization. It is a process to reduce a word to its root form, called a lemma. For example the verb 'planning' would be changed to 'plan' world. + +# %% +# Lemmatization +import en_core_web_sm + +nlp = en_core_web_sm.load() + +def lemmatize_text(text): + doc = nlp(text) + return " ".join([token.lemma_ for token in doc]) + +# %% +new_df['job_posting'] = new_df['job_posting'].apply(lemmatize_text) + +new_df.head() + +# %% [markdown] +# At this stage we can also visualize the data with wordcloud by having special text column. We can show it for both fake and real dataset. + +# %% +from wordcloud import WordCloud + +non_fraudulent_text = ' '.join(text for text in new_df[new_df['fraudulent'] == 0]['job_posting']) +fraudulent_text = ' '.join(text for text in new_df[new_df['fraudulent'] == 1]['job_posting']) + +wordcloud_non_fraudulent = WordCloud(width=800, height=400, background_color='white').generate(non_fraudulent_text) + +wordcloud_fraudulent = WordCloud(width=800, height=400, background_color='white').generate(fraudulent_text) + +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 10)) + +ax1.imshow(wordcloud_non_fraudulent, interpolation='bilinear') +ax1.axis('off') +ax1.set_title('Non-Fraudulent Job Postings') + +ax2.imshow(wordcloud_fraudulent, interpolation='bilinear') +ax2.axis('off') +ax2.set_title('Fraudulent Job Postings') + +plt.show() + +# %% [markdown] +# ## Fake job postings identification and removal +# +# Nowadays, it is unfortunate that not all the job offers that are posted on papular portals are genuine. Some of them are created only to collect personal data. Therefore, just detecting fake job postings can be very essential. +# +# We will create bidirectional LSTM model with one hot encoding. Let's start with all necessary imports. + +# %% +from tensorflow.keras.layers import Embedding +from tensorflow.keras.preprocessing.sequence import pad_sequences +from tensorflow.keras.models import Sequential +from tensorflow.keras.preprocessing.text import one_hot +from tensorflow.keras.layers import Dense +from tensorflow.keras.layers import Bidirectional +from tensorflow.keras.layers import Dropout + +# %% [markdown] +# Make sure, you're using Tensorflow version 2.15.0 + +# %% +import tensorflow as tf +tf.__version__ + +# %% [markdown] +# Now, let us import Intel Extension for TensorFlow*. We are using Python API `itex.experimental_ops_override()`. It automatically replace some TensorFlow operators by Custom Operators under `itex.ops` namespace, as well as to be compatible with existing trained parameters. + +# %% +import intel_extension_for_tensorflow as itex + +itex.experimental_ops_override() + +# %% [markdown] +# We need to prepare data for the model we will create. First let's assign job_postings to X and fraudulent values to y (expected value). + +# %% +X = new_df['job_posting'] +y = new_df['fraudulent'] + +# %% [markdown] +# One hot encoding is a technique to represent categorical variables as numerical values. + +# %% +voc_size = 5000 +onehot_repr = [one_hot(words, voc_size) for words in X] + +# %% +sent_length = 40 +embedded_docs = pad_sequences(onehot_repr, padding='pre', maxlen=sent_length) +print(embedded_docs) + +# %% [markdown] +# ### Creating model +# +# We are creating Deep Neural Network using Bidirectional LSTM. The architecture is as followed: +# +# * Embedding layer +# * Bidirectiona LSTM Layer +# * Dropout layer +# * Dense layer with sigmod function +# +# We are using Adam optimizer with binary crossentropy. We are optimism accuracy. +# +# If Intel® Extension for TensorFlow* backend is XPU, `tf.keras.layers.LSTM` will be replaced by `itex.ops.ItexLSTM`. + +# %% +embedding_vector_features = 50 +model_itex = Sequential() +model_itex.add(Embedding(voc_size, embedding_vector_features, input_length=sent_length)) +model_itex.add(Bidirectional(itex.ops.ItexLSTM(100))) +model_itex.add(Dropout(0.3)) +model_itex.add(Dense(1, activation='sigmoid')) +model_itex.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) +print(model_itex.summary()) + +# %% +X_final = np.array(embedded_docs) +y_final = np.array(y) + +# %% [markdown] +# + +# %% +from sklearn.model_selection import train_test_split +X_train, X_test, y_train, y_test = train_test_split(X_final, y_final, test_size=0.25, random_state=320) + +# %% [markdown] +# Now, let's train the model. We are using standard `model.fit()` method providing training and testing dataset. You can easily modify number of epochs in this training process but keep in mind that the model can become overtrained, so that it will have very good results on training data, but poor results on test data. + +# %% +model_itex.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=1, batch_size=64) + +# %% [markdown] +# The values returned by the model are in the range [0,1] Need to map them to integer values of 0 or 1. + +# %% +y_pred = (model_itex.predict(X_test) > 0.5).astype("int32") + +# %% [markdown] +# To demonstrate the effectiveness of our models we presented the confusion matrix and classification report available within the `scikit-learn` library. + +# %% +from sklearn.metrics import ConfusionMatrixDisplay, confusion_matrix, classification_report + +conf_matrix = confusion_matrix(y_test, y_pred) +print("Confusion matrix:") +print(conf_matrix) + +ConfusionMatrixDisplay.from_predictions(y_test, y_pred) + +class_report = classification_report(y_test, y_pred) +print("Classification report:") +print(class_report) + +# %% [markdown] +# ## Job recommendation by showing the most similar ones + +# %% [markdown] +# Now, as we are sure that the data we are processing is real, we can get back to the original columns and create our recommendation system. +# +# Also use much more simple solution for recommendations. Even, as before we used Deep Learning to check if posting is fake, we can use classical machine learning algorithms to show similar job postings. +# +# First, let's filter fake job postings. + +# %% +real = df[df['fraudulent'] == 0] +real.head() + +# %% [markdown] +# After that, we create a common column containing those text parameters that we want to be compared between theses and are relevant to us when making recommendations. + +# %% +cols = ['title', 'description', 'requirements', 'required_experience', 'required_education', 'industry'] +real = real[cols] +real.head() + +# %% +real = real.fillna(value='') +real['text'] = real['description'] + real['requirements'] + real['required_experience'] + real['required_education'] + real['industry'] +real.head() + +# %% [markdown] +# Let's see the mechanism that we will use to prepare recommendations - we will use sentence similarity based on prepared `text` column in our dataset. + +# %% +from sentence_transformers import SentenceTransformer + +model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2') + +# %% [markdown] +# Let's prepare a few example sentences that cover 4 topics. On these sentences it will be easier to show how the similarities between the texts work than on the whole large dataset we have. + +# %% +messages = [ + # Smartphones + "I like my phone", + "My phone is not good.", + "Your cellphone looks great.", + + # Weather + "Will it snow tomorrow?", + "Recently a lot of hurricanes have hit the US", + "Global warming is real", + + # Food and health + "An apple a day, keeps the doctors away", + "Eating strawberries is healthy", + "Is paleo better than keto?", + + # Asking about age + "How old are you?", + "what is your age?", +] + +# %% [markdown] +# Now, we are preparing functions to show similarities between given sentences in the for of heat map. + +# %% +import numpy as np +import seaborn as sns + +def plot_similarity(labels, features, rotation): + corr = np.inner(features, features) + sns.set(font_scale=1.2) + g = sns.heatmap( + corr, + xticklabels=labels, + yticklabels=labels, + vmin=0, + vmax=1, + cmap="YlOrRd") + g.set_xticklabels(labels, rotation=rotation) + g.set_title("Semantic Textual Similarity") + +def run_and_plot(messages_): + message_embeddings_ = model.encode(messages_) + plot_similarity(messages_, message_embeddings_, 90) + +# %% +run_and_plot(messages) + +# %% [markdown] +# Now, let's move back to our job postings dataset. First, we are using sentence encoding model to be able to calculate similarities. + +# %% +encodings = [] +for text in real['text']: + encodings.append(model.encode(text)) + +real['encodings'] = encodings + +# %% [markdown] +# Then, we can chose job posting we wan to calculate similarities to. In our case it is first job posting in the dataset, but you can easily change it to any other job posting, by changing value in the `index` variable. + +# %% +index = 0 +corr = np.inner(encodings[index], encodings) +real['corr_to_first'] = corr + +# %% [markdown] +# And based on the calculated similarities, we can show top most similar job postings, by sorting them according to calculated correlation value. + +# %% +real.sort_values(by=['corr_to_first'], ascending=False).head() + +# %% [markdown] +# In this code sample we created job recommendation system. First, we explored and analyzed the dataset, then we pre-process the data and create fake job postings detection model. At the end we used sentence similarities to show top 5 recommendations - the most similar job descriptions to the chosen one. + +# %% +print("[CODE_SAMPLE_COMPLETED_SUCCESSFULLY]") + + diff --git a/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/License.txt b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/License.txt new file mode 100644 index 0000000000..e63c6e13dc --- /dev/null +++ b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/License.txt @@ -0,0 +1,7 @@ +Copyright Intel Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/README.md b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/README.md new file mode 100644 index 0000000000..6964819ee4 --- /dev/null +++ b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/README.md @@ -0,0 +1,177 @@ +# Job Recommendation System: End-to-End Deep Learning Workload + + +This sample illustrates the use of Intel® Extension for TensorFlow* to build and run an end-to-end AI workload on the example of the job recommendation system. + +| Property | Description +|:--- |:--- +| Category | Reference Designs and End to End +| What you will learn | How to use Intel® Extension for TensorFlow* to build end to end AI workload? +| Time to complete | 30 minutes + +## Purpose + +This code sample show end-to-end Deep Learning workload in the example of job recommendation system. It consists of four main parts: + +1. Data exploration and visualization - showing what the dataset is looking like, what are some of the main features and what is a data distribution in it. +2. Data cleaning and pre-processing - removal of duplicates, explanation all necessary steps for text pre-processing. +3. Fraud job postings removal - finding which of the job posting are fake using LSTM DNN and filtering them. +4. Job recommendation - calculation and providing top-n job descriptions similar to the chosen one. + +## Prerequisites + +| Optimized for | Description +| :--- | :--- +| OS | Linux, Ubuntu* 20.04 +| Hardware | GPU +| Software | Intel® Extension for TensorFlow* +> **Note**: AI and Analytics samples are validated on AI Tools Offline Installer. For the full list of validated platforms refer to [Platform Validation](https://github.com/oneapi-src/oneAPI-samples/tree/master?tab=readme-ov-file#platform-validation). + + +## Key Implementation Details + +This sample creates Deep Neural Networ to fake job postings detections using Intel® Extension for TensorFlow* LSTM layer on GPU. It also utilizes `itex.experimental_ops_override()` to automatically replace some TensorFlow operators by Custom Operators form Intel® Extension for TensorFlow*. + +The sample tutorial contains one Jupyter Notebook and one Python script. You can use either. + +## Environment Setup +You will need to download and install the following toolkits, tools, and components to use the sample. + + +**1. Get AI Tools** + +Required AI Tools: + +If you have not already, select and install these Tools via [AI Tools Selector](https://www.intel.com/content/www/us/en/developer/tools/oneapi/ai-tools-selector.html). AI and Analytics samples are validated on AI Tools Offline Installer. It is recommended to select Offline Installer option in AI Tools Selector. + +>**Note**: If Docker option is chosen in AI Tools Selector, refer to [Working with Preset Containers](https://github.com/intel/ai-containers/tree/main/preset) to learn how to run the docker and samples. + +**2. (Offline Installer) Activate the AI Tools bundle base environment** + +If the default path is used during the installation of AI Tools: +``` +source $HOME/intel/oneapi/intelpython/bin/activate +``` +If a non-default path is used: +``` +source /bin/activate +``` + +**3. (Offline Installer) Activate relevant Conda environment** + +``` +conda activate tensorflow-gpu +``` + +**4. Clone the GitHub repository** + + +``` +git clone https://github.com/oneapi-src/oneAPI-samples.git +cd oneAPI-samples/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem +``` + +**5. Install dependencies** + +>**Note**: Before running the following commands, make sure your Conda/Python environment with AI Tools installed is activated + +``` +pip install -r requirements.txt +pip install notebook +``` +For Jupyter Notebook, refer to [Installing Jupyter](https://jupyter.org/install) for detailed installation instructions. + +## Run the Sample +>**Note**: Before running the sample, make sure [Environment Setup](https://github.com/oneapi-src/oneAPI-samples/tree/master/AI-and-Analytics/Getting-Started-Samples/INC-Quantization-Sample-for-PyTorch#environment-setup) is completed. + +Go to the section which corresponds to the installation method chosen in [AI Tools Selector](https://www.intel.com/content/www/us/en/developer/tools/oneapi/ai-tools-selector.html) to see relevant instructions: +* [AI Tools Offline Installer (Validated)](#ai-tools-offline-installer-validated) +* [Conda/PIP](#condapip) +* [Docker](#docker) + +### AI Tools Offline Installer (Validated) + +**1. Register Conda kernel to Jupyter Notebook kernel** + +If the default path is used during the installation of AI Tools: +``` +$HOME/intel/oneapi/intelpython/envs/tensorflow-gpu/bin/python -m ipykernel install --user --name=tensorflow-gpu +``` +If a non-default path is used: +``` +/bin/python -m ipykernel install --user --name=tensorflow-gpu +``` +**2. Launch Jupyter Notebook** + +``` +jupyter notebook --ip=0.0.0.0 +``` +**3. Follow the instructions to open the URL with the token in your browser** + +**4. Select the Notebook** + +``` +JobRecommendationSystem.ipynb +``` +**5. Change the kernel to `tensorflow-gpu`** + +**6. Run every cell in the Notebook in sequence** + +### Conda/PIP +> **Note**: Before running the instructions below, make sure your Conda/Python environment with AI Tools installed is activated + +**1. Register Conda/Python kernel to Jupyter Notebook kernel** + +For Conda: +``` +/bin/python -m ipykernel install --user --name=tensorflow-gpu +``` +To know , run `conda env list` and find your Conda environment path. + +For PIP: +``` +python -m ipykernel install --user --name=tensorflow-gpu +``` +**2. Launch Jupyter Notebook** + +``` +jupyter notebook --ip=0.0.0.0 +``` +**3. Follow the instructions to open the URL with the token in your browser** + +**4. Select the Notebook** + +``` +JobRecommendationSystem.ipynb +``` +**5. Change the kernel to ``** + + +**6. Run every cell in the Notebook in sequence** + +### Docker +AI Tools Docker images already have Get Started samples pre-installed. Refer to [Working with Preset Containers](https://github.com/intel/ai-containers/tree/main/preset) to learn how to run the docker and samples. + + + +## Example Output + + If successful, the sample displays [CODE_SAMPLE_COMPLETED_SUCCESSFULLY]. Additionally, the sample shows multiple diagram explaining dataset, the training progress for fraud job posting detection and top job recommendations. + +## Related Samples + + +* [Intel Extension For TensorFlow Getting Started Sample](https://github.com/oneapi-src/oneAPI-samples/blob/development/AI-and-Analytics/Getting-Started-Samples/Intel_Extension_For_TensorFlow_GettingStarted/README.md) +* [Leveraging Intel Extension for TensorFlow with LSTM for Text Generation Sample](https://github.com/oneapi-src/oneAPI-samples/blob/master/AI-and-Analytics/Features-and-Functionality/IntelTensorFlow_TextGeneration_with_LSTM/README.md) + +## License + +Code samples are licensed under the MIT license. See +[License.txt](https://github.com/oneapi-src/oneAPI-samples/blob/master/License.txt) +for details. + +Third party program Licenses can be found here: +[third-party-programs.txt](https://github.com/oneapi-src/oneAPI-samples/blob/master/third-party-programs.txt) + +*Other names and brands may be claimed as the property of others. [Trademarks](https://www.intel.com/content/www/us/en/legal/trademarks.html) diff --git a/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/requirements.txt b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/requirements.txt new file mode 100644 index 0000000000..15bcd710c6 --- /dev/null +++ b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/requirements.txt @@ -0,0 +1,10 @@ +ipykernel +matplotlib +sentence_transformers +transformers +datasets +accelerate +wordcloud +spacy +jinja2 +nltk \ No newline at end of file diff --git a/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/sample.json b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/sample.json new file mode 100644 index 0000000000..31e14cab36 --- /dev/null +++ b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/sample.json @@ -0,0 +1,29 @@ +{ + "guid": "80708728-0BD4-435E-961D-178E5ED1450C", + "name": "JobRecommendationSystem: End-to-End Deep Learning Workload", + "categories": ["Toolkit/oneAPI AI And Analytics/End-to-End Workloads"], + "description": "This sample illustrates the use of Intel® Extension for TensorFlow* to build and run an end-to-end AI workload on the example of the job recommendation system", + "builder": ["cli"], + "toolchain": ["jupyter"], + "languages": [{"python":{}}], + "os":["linux"], + "targetDevice": ["GPU"], + "ciTests": { + "linux": [ + { + "env": [], + "id": "JobRecommendationSystem_py", + "steps": [ + "source /intel/oneapi/intelpython/bin/activate", + "conda env remove -n user_tensorflow-gpu", + "conda create --name user_tensorflow-gpu --clone tensorflow-gpu", + "conda activate user_tensorflow-gpu", + "pip install -r requirements.txt", + "python -m ipykernel install --user --name=user_tensorflow-gpu", + "python JobRecommendationSystem.py" + ] + } + ] +}, +"expertise": "Reference Designs and End to End" +} \ No newline at end of file diff --git a/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/third-party-programs.txt b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/third-party-programs.txt new file mode 100644 index 0000000000..e9f8042d0a --- /dev/null +++ b/AI-and-Analytics/End-to-end-Workloads/JobRecommendationSystem/third-party-programs.txt @@ -0,0 +1,253 @@ +oneAPI Code Samples - Third Party Programs File + +This file contains the list of third party software ("third party programs") +contained in the Intel software and their required notices and/or license +terms. This third party software, even if included with the distribution of the +Intel software, may be governed by separate license terms, including without +limitation, third party license terms, other Intel software license terms, and +open source software license terms. These separate license terms govern your use +of the third party programs as set forth in the “third-party-programs.txt” or +other similarly named text file. + +Third party programs and their corresponding required notices and/or license +terms are listed below. + +-------------------------------------------------------------------------------- + +1. Nothings STB Libraries + +stb/LICENSE + + This software is available under 2 licenses -- choose whichever you prefer. + ------------------------------------------------------------------------------ + ALTERNATIVE A - MIT License + Copyright (c) 2017 Sean Barrett + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is furnished to do + so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + ------------------------------------------------------------------------------ + ALTERNATIVE B - Public Domain (www.unlicense.org) + This is free and unencumbered software released into the public domain. + Anyone is free to copy, modify, publish, use, compile, sell, or distribute this + software, either in source code form or as a compiled binary, for any purpose, + commercial or non-commercial, and by any means. + In jurisdictions that recognize copyright laws, the author or authors of this + software dedicate any and all copyright interest in the software to the public + domain. We make this dedication for the benefit of the public at large and to + the detriment of our heirs and successors. We intend this dedication to be an + overt act of relinquishment in perpetuity of all present and future rights to + this software under copyright law. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +-------------------------------------------------------------------------------- + +2. FGPA example designs-gzip + + SDL2.0 + +zlib License + + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + +-------------------------------------------------------------------------------- + +3. Nbody + (c) 2019 Fabio Baruffa + + Plotly.js + Copyright (c) 2020 Plotly, Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +© 2020 GitHub, Inc. + +-------------------------------------------------------------------------------- + +4. GNU-EFI + Copyright (c) 1998-2000 Intel Corporation + +The files in the "lib" and "inc" subdirectories are using the EFI Application +Toolkit distributed by Intel at http://developer.intel.com/technology/efi + +This code is covered by the following agreement: + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INTEL BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. THE EFI SPECIFICATION AND ALL OTHER INFORMATION +ON THIS WEB SITE ARE PROVIDED "AS IS" WITH NO WARRANTIES, AND ARE SUBJECT +TO CHANGE WITHOUT NOTICE. + +-------------------------------------------------------------------------------- + +5. Edk2 + Copyright (c) 2019, Intel Corporation. All rights reserved. + + Edk2 Basetools + Copyright (c) 2019, Intel Corporation. All rights reserved. + +SPDX-License-Identifier: BSD-2-Clause-Patent + +-------------------------------------------------------------------------------- + +6. Heat Transmission + +GNU LESSER GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. + +0. Additional Definitions. +As used herein, “this License” refers to version 3 of the GNU Lesser General Public License, and the “GNU GPL” refers to version 3 of the GNU General Public License. + +“The Library” refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. + +An “Application” is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. + +A “Combined Work” is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the “Linked Version”. + +The “Minimal Corresponding Source” for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. + +The “Corresponding Application Code” for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. + +1. Exception to Section 3 of the GNU GPL. +You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. + +2. Conveying Modified Versions. +If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: + +a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or +b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. +3. Object Code Incorporating Material from Library Header Files. +The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: + +a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. +b) Accompany the object code with a copy of the GNU GPL and this license document. +4. Combined Works. +You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: + +a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. +b) Accompany the Combined Work with a copy of the GNU GPL and this license document. +c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. +d) Do one of the following: +0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. +1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. +e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) +5. Combined Libraries. +You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: + +a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. +b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. +6. Revised Versions of the GNU Lesser General Public License. +The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. + +If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. + +-------------------------------------------------------------------------------- +7. Rodinia + Copyright (c)2008-2011 University of Virginia +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted without royalty fees or other restrictions, provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of the University of Virginia, the Dept. of Computer Science, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF VIRGINIA OR THE SOFTWARE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +If you use this software or a modified version of it, please cite the most relevant among the following papers: + + - M. A. Goodrum, M. J. Trotter, A. Aksel, S. T. Acton, and K. Skadron. Parallelization of Particle Filter Algorithms. In Proceedings of the 3rd Workshop on Emerging Applications and Many-core Architecture (EAMA), in conjunction with the IEEE/ACM International +Symposium on Computer Architecture (ISCA), June 2010. + + - S. Che, M. Boyer, J. Meng, D. Tarjan, J. W. Sheaffer, Sang-Ha Lee and K. Skadron. +Rodinia: A Benchmark Suite for Heterogeneous Computing. IEEE International Symposium +on Workload Characterization, Oct 2009. + +- J. Meng and K. Skadron. "Performance Modeling and Automatic Ghost Zone Optimization +for Iterative Stencil Loops on GPUs." In Proceedings of the 23rd Annual ACM International +Conference on Supercomputing (ICS), June 2009. + +- L.G. Szafaryn, K. Skadron and J. Saucerman. "Experiences Accelerating MATLAB Systems +Biology Applications." in Workshop on Biomedicine in Computing (BiC) at the International +Symposium on Computer Architecture (ISCA), June 2009. + +- M. Boyer, D. Tarjan, S. T. Acton, and K. Skadron. "Accelerating Leukocyte Tracking using CUDA: +A Case Study in Leveraging Manycore Coprocessors." In Proceedings of the International Parallel +and Distributed Processing Symposium (IPDPS), May 2009. + +- S. Che, M. Boyer, J. Meng, D. Tarjan, J. W. Sheaffer, and K. Skadron. "A Performance +Study of General Purpose Applications on Graphics Processors using CUDA" Journal of +Parallel and Distributed Computing, Elsevier, June 2008. + +-------------------------------------------------------------------------------- +Other names and brands may be claimed as the property of others. + +-------------------------------------------------------------------------------- \ No newline at end of file diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Dataset/get_dataset.py b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Dataset/get_dataset.py new file mode 100644 index 0000000000..f30a8d06e7 --- /dev/null +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Dataset/get_dataset.py @@ -0,0 +1,35 @@ +import os +import shutil +import argparse +from datasets import load_dataset +from tqdm import tqdm + +language_to_code = { + "japanese": "ja", + "swedish": "sv-SE" +} + +def download_dataset(output_dir): + for lang, lang_code in language_to_code.items(): + print(f"Processing dataset for language: {lang_code}") + + # Load the dataset for the specific language + dataset = load_dataset("mozilla-foundation/common_voice_11_0", lang_code, split="train", trust_remote_code=True) + + # Create a language-specific output folder + output_folder = os.path.join(output_dir, lang, lang_code, "clips") + os.makedirs(output_folder, exist_ok=True) + + # Extract and copy MP3 files + for sample in tqdm(dataset, desc=f"Extracting and copying MP3 files for {lang}"): + audio_path = sample['audio']['path'] + shutil.copy(audio_path, output_folder) + + print("Extraction and copy complete.") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Extract and copy audio files from a dataset to a specified directory.") + parser.add_argument("--output_dir", type=str, default="/data/commonVoice", help="Base output directory for saving the files. Default is /data/commonVoice") + args = parser.parse_args() + + download_dataset(args.output_dir) \ No newline at end of file diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/clean.sh b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/clean.sh index 7ea1719af4..34747af45c 100644 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/clean.sh +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/clean.sh @@ -1,7 +1,5 @@ #!/bin/bash -rm -R RIRS_NOISES -rm -R tmp -rm -R speechbrain -rm -f rirs_noises.zip noise.csv reverb.csv vad_file.txt +echo "Deleting .wav files, tmp" rm -f ./*.wav +rm -R tmp diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/inference_commonVoice.py b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/inference_commonVoice.py index 6442418bf0..7effb2df76 100644 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/inference_commonVoice.py +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/inference_commonVoice.py @@ -29,7 +29,7 @@ def __init__(self, dirpath, filename): self.sampleRate = 0 self.waveData = '' self.wavesize = 0 - self.waveduriation = 0 + self.waveduration = 0 if filename.endswith(".wav") or filename.endswith(".wmv"): self.wavefile = filename self.wavepath = dirpath + os.sep + filename @@ -173,12 +173,12 @@ def main(argv): data = datafile(testDataDirectory, filename) predict_list = [] use_entire_audio_file = False - if data.waveduration < sample_dur: + if int(data.waveduration) <= sample_dur: # Use entire audio file if the duration is less than the sampling duration use_entire_audio_file = True sample_list = [0 for _ in range(sample_size)] else: - start_time_list = list(range(sample_size - int(data.waveduration) + 1)) + start_time_list = list(range(0, int(data.waveduration) - sample_dur)) sample_list = [] for i in range(sample_size): sample_list.append(random.sample(start_time_list, 1)[0]) @@ -198,10 +198,6 @@ def main(argv): predict_list.append(' ') pass - # Clean up - if use_entire_audio_file: - os.remove("./" + data.filename) - # Pick the top rated prediction result occurence_count = Counter(predict_list) total_count = sum(occurence_count.values()) diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/inference_custom.py b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/inference_custom.py index b4f9d6adee..2b4a331c0b 100644 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/inference_custom.py +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/inference_custom.py @@ -30,7 +30,7 @@ def __init__(self, dirpath, filename): self.sampleRate = 0 self.waveData = '' self.wavesize = 0 - self.waveduriation = 0 + self.waveduration = 0 if filename.endswith(".wav") or filename.endswith(".wmv"): self.wavefile = filename self.wavepath = dirpath + os.sep + filename @@ -61,41 +61,45 @@ def __init__(self, ipex_op=False, bf16=False, int8_model=False): self.model_int8 = load(source_model_int8_path, self.language_id) self.model_int8.eval() elif ipex_op: + self.language_id.eval() + # Optimize for inference with IPEX print("Optimizing inference with IPEX") - self.language_id.eval() - sampleInput = (torch.load("./sample_input_features.pt"), torch.load("./sample_input_wav_lens.pt")) if bf16: print("BF16 enabled") self.language_id.mods["compute_features"] = ipex.optimize(self.language_id.mods["compute_features"], dtype=torch.bfloat16) self.language_id.mods["mean_var_norm"] = ipex.optimize(self.language_id.mods["mean_var_norm"], dtype=torch.bfloat16) - self.language_id.mods["embedding_model"] = ipex.optimize(self.language_id.mods["embedding_model"], dtype=torch.bfloat16) self.language_id.mods["classifier"] = ipex.optimize(self.language_id.mods["classifier"], dtype=torch.bfloat16) else: self.language_id.mods["compute_features"] = ipex.optimize(self.language_id.mods["compute_features"]) self.language_id.mods["mean_var_norm"] = ipex.optimize(self.language_id.mods["mean_var_norm"]) - self.language_id.mods["embedding_model"] = ipex.optimize(self.language_id.mods["embedding_model"]) self.language_id.mods["classifier"] = ipex.optimize(self.language_id.mods["classifier"]) # Torchscript to resolve performance issues with reorder operations + print("Applying Torchscript") + sampleWavs = torch.load("./sample_wavs.pt") + sampleWavLens = torch.ones(sampleWavs.shape[0]) with torch.no_grad(): - I2 = self.language_id.mods["embedding_model"](*sampleInput) + I1 = self.language_id.mods["compute_features"](sampleWavs) + I2 = self.language_id.mods["mean_var_norm"](I1, sampleWavLens) + I3 = self.language_id.mods["embedding_model"](I2, sampleWavLens) + if bf16: with torch.cpu.amp.autocast(): - self.language_id.mods["compute_features"] = torch.jit.trace( self.language_id.mods["compute_features"] , example_inputs=(torch.rand(1,32000))) - self.language_id.mods["mean_var_norm"] = torch.jit.trace(self.language_id.mods["mean_var_norm"], example_inputs=sampleInput) - self.language_id.mods["embedding_model"] = torch.jit.trace(self.language_id.mods["embedding_model"], example_inputs=sampleInput) - self.language_id.mods["classifier"] = torch.jit.trace(self.language_id.mods["classifier"], example_inputs=I2) + self.language_id.mods["compute_features"] = torch.jit.trace( self.language_id.mods["compute_features"] , example_inputs=sampleWavs) + self.language_id.mods["mean_var_norm"] = torch.jit.trace(self.language_id.mods["mean_var_norm"], example_inputs=(I1, sampleWavLens)) + self.language_id.mods["embedding_model"] = torch.jit.trace(self.language_id.mods["embedding_model"], example_inputs=(I2, sampleWavLens)) + self.language_id.mods["classifier"] = torch.jit.trace(self.language_id.mods["classifier"], example_inputs=I3) self.language_id.mods["compute_features"] = torch.jit.freeze(self.language_id.mods["compute_features"]) self.language_id.mods["mean_var_norm"] = torch.jit.freeze(self.language_id.mods["mean_var_norm"]) self.language_id.mods["embedding_model"] = torch.jit.freeze(self.language_id.mods["embedding_model"]) self.language_id.mods["classifier"] = torch.jit.freeze( self.language_id.mods["classifier"]) else: - self.language_id.mods["compute_features"] = torch.jit.trace( self.language_id.mods["compute_features"] , example_inputs=(torch.rand(1,32000))) - self.language_id.mods["mean_var_norm"] = torch.jit.trace(self.language_id.mods["mean_var_norm"], example_inputs=sampleInput) - self.language_id.mods["embedding_model"] = torch.jit.trace(self.language_id.mods["embedding_model"], example_inputs=sampleInput) - self.language_id.mods["classifier"] = torch.jit.trace(self.language_id.mods["classifier"], example_inputs=I2) + self.language_id.mods["compute_features"] = torch.jit.trace( self.language_id.mods["compute_features"] , example_inputs=sampleWavs) + self.language_id.mods["mean_var_norm"] = torch.jit.trace(self.language_id.mods["mean_var_norm"], example_inputs=(I1, sampleWavLens)) + self.language_id.mods["embedding_model"] = torch.jit.trace(self.language_id.mods["embedding_model"], example_inputs=(I2, sampleWavLens)) + self.language_id.mods["classifier"] = torch.jit.trace(self.language_id.mods["classifier"], example_inputs=I3) self.language_id.mods["compute_features"] = torch.jit.freeze(self.language_id.mods["compute_features"]) self.language_id.mods["mean_var_norm"] = torch.jit.freeze(self.language_id.mods["mean_var_norm"]) @@ -114,11 +118,11 @@ def predict(self, data_path="", ipex_op=False, bf16=False, int8_model=False, ver with torch.no_grad(): if bf16: with torch.cpu.amp.autocast(): - prediction = self.language_id.classify_batch(signal) + prediction = self.language_id.classify_batch(signal) else: - prediction = self.language_id.classify_batch(signal) + prediction = self.language_id.classify_batch(signal) else: # default - prediction = self.language_id.classify_batch(signal) + prediction = self.language_id.classify_batch(signal) inference_end_time = time() inference_latency = inference_end_time - inference_start_time @@ -195,13 +199,13 @@ def main(argv): with open(OUTPUT_SUMMARY_CSV_FILE, 'w') as f: writer = csv.writer(f) writer.writerow(["Audio File", - "Input Frequency", + "Input Frequency (Hz)", "Expected Language", "Top Consensus", "Top Consensus %", "Second Consensus", "Second Consensus %", - "Average Latency", + "Average Latency (s)", "Result"]) total_samples = 0 @@ -273,12 +277,12 @@ def main(argv): predict_list = [] use_entire_audio_file = False latency_sum = 0.0 - if data.waveduration < sample_dur: + if int(data.waveduration) <= sample_dur: # Use entire audio file if the duration is less than the sampling duration use_entire_audio_file = True sample_list = [0 for _ in range(sample_size)] else: - start_time_list = list(range(sample_size - int(data.waveduration) + 1)) + start_time_list = list(range(int(data.waveduration) - sample_dur)) sample_list = [] for i in range(sample_size): sample_list.append(random.sample(start_time_list, 1)[0]) @@ -346,17 +350,36 @@ def main(argv): avg_latency, result ]) + else: + # Write results to a .csv file + with open(OUTPUT_SUMMARY_CSV_FILE, 'a') as f: + writer = csv.writer(f) + writer.writerow([ + filename, + sample_rate_for_csv, + "N/A", + top_occurance, + str(topPercentage) + "%", + sec_occurance, + str(secPercentage) + "%", + avg_latency, + "N/A" + ]) + if ground_truth_compare: # Summary of results print("\n\n Correctly predicted %d/%d\n" %(correct_predictions, total_samples)) - print("\n See %s for summary\n" %(OUTPUT_SUMMARY_CSV_FILE)) + + print("\n See %s for summary\n" %(OUTPUT_SUMMARY_CSV_FILE)) elif os.path.isfile(path): print("\nIt is a normal file", path) else: print("It is a special file (socket, FIFO, device file)" , path) + print("Done.\n") + if __name__ == "__main__": import sys sys.exit(main(sys.argv)) diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/initialize.sh b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/initialize.sh deleted file mode 100644 index 935debac44..0000000000 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/initialize.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -# Activate the oneAPI environment for PyTorch -source activate pytorch - -# Install speechbrain -git clone https://github.com/speechbrain/speechbrain.git -cd speechbrain -pip install -r requirements.txt -pip install --editable . -cd .. - -# Add speechbrain to environment variable PYTHONPATH -export PYTHONPATH=$PYTHONPATH:/Inference/speechbrain - -# Install PyTorch and Intel Extension for PyTorch (IPEX) -pip install torch==1.13.1 torchaudio -pip install --no-deps torchvision==0.14.0 -pip install intel_extension_for_pytorch==1.13.100 -pip install neural-compressor==2.0 - -# Update packages -apt-get update && apt-get install libgl1 diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/interfaces.patch b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/interfaces.patch deleted file mode 100644 index 762ae5ebee..0000000000 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/interfaces.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- interfaces.py 2022-10-07 16:58:26.836359346 -0700 -+++ interfaces_new.py 2022-10-07 16:59:09.968110128 -0700 -@@ -945,7 +945,7 @@ - out_prob = self.mods.classifier(emb).squeeze(1) - score, index = torch.max(out_prob, dim=-1) - text_lab = self.hparams.label_encoder.decode_torch(index) -- return out_prob, score, index, text_lab -+ return out_prob, score, index # removed text_lab to get torchscript to work - - def classify_file(self, path): - """Classifies the given audiofile into the given set of labels. diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/lang_id_inference.ipynb b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/lang_id_inference.ipynb index 0ed44139b3..1cd1afee01 100644 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/lang_id_inference.ipynb +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/lang_id_inference.ipynb @@ -47,7 +47,7 @@ "metadata": {}, "outputs": [], "source": [ - "!python inference_commonVoice.py -p /data/commonVoice/test" + "!python inference_commonVoice.py -p ${COMMON_VOICE_PATH}/processed_data/test" ] }, { @@ -55,7 +55,7 @@ "metadata": {}, "source": [ "## inference_custom.py for Custom Data \n", - "To generate an overall results output summary, the audio_ground_truth_labels.csv file needs to be modified with the name of the audio file and expected audio label (i.e. en for English). By default, this is disabled but if desired, the *--ground_truth_compare* can be used. To run inference on custom data, you must specify a folder with WAV files and pass the path in as an argument. " + "To run inference on custom data, you must specify a folder with .wav files and pass the path in as an argument. You can do so by creating a folder named `data_custom` and then copy 1 or 2 .wav files from your test dataset into it. .mp3 files will NOT work. " ] }, { @@ -65,7 +65,7 @@ "### Randomly select audio clips from audio files for prediction\n", "python inference_custom.py -p DATAPATH -d DURATION -s SIZE\n", "\n", - "An output file output_summary.csv will give the summary of the results." + "An output file `output_summary.csv` will give the summary of the results." ] }, { @@ -104,6 +104,8 @@ "### Optimizations with Intel® Extension for PyTorch (IPEX) \n", "python inference_custom.py -p data_custom -d 3 -s 50 --vad --ipex --verbose \n", "\n", + "This will apply ipex.optimize to the model(s) and TorchScript. You can also add the --bf16 option along with --ipex to run in the BF16 data type, supported on 4th Gen Intel® Xeon® Scalable processors and newer.\n", + "\n", "Note that the *--verbose* option is required to view the latency measurements. " ] }, @@ -121,7 +123,7 @@ "metadata": {}, "source": [ "## Quantization with Intel® Neural Compressor (INC)\n", - "To improve inference latency, Intel® Neural Compressor (INC) can be used to quantize the trained model from FP32 to INT8 by running quantize_model.py. The *-datapath* argument can be used to specify a custom evaluation dataset but by default it is set to */data/commonVoice/dev* which was generated from the data preprocessing scripts in the *Training* folder. " + "To improve inference latency, Intel® Neural Compressor (INC) can be used to quantize the trained model from FP32 to INT8 by running quantize_model.py. The *-datapath* argument can be used to specify a custom evaluation dataset but by default it is set to `$COMMON_VOICE_PATH/processed_data/dev` which was generated from the data preprocessing scripts in the `Training` folder. " ] }, { @@ -130,14 +132,46 @@ "metadata": {}, "outputs": [], "source": [ - "!python quantize_model.py -p ./lang_id_commonvoice_model -datapath $COMMON_VOICE_PATH/dev" + "!python quantize_model.py -p ./lang_id_commonvoice_model -datapath $COMMON_VOICE_PATH/processed_data/dev" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After quantization, the model will be stored in lang_id_commonvoice_model_INT8 and neural_compressor.utils.pytorch.load will have to be used to load the quantized model for inference. If self.language_id is the original model and data_path is the path to the audio file:\n", + "\n", + "```\n", + "from neural_compressor.utils.pytorch import load\n", + "model_int8 = load(\"./lang_id_commonvoice_model_INT8\", self.language_id)\n", + "signal = self.language_id.load_audio(data_path)\n", + "prediction = self.model_int8(signal)\n", + "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "After quantization, the model will be stored in *lang_id_commonvoice_model_INT8* and *neural_compressor.utils.pytorch.load* will have to be used to load the quantized model for inference. " + "The code above is integrated into inference_custom.py. You can now run inference on your data using this INT8 model:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!python inference_custom.py -p data_custom -d 3 -s 50 --vad --int8_model --verbose" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### (Optional) Comparing Predictions with Ground Truth\n", + "\n", + "You can choose to modify audio_ground_truth_labels.csv to include the name of the audio file and expected audio label (like, en for English), then run inference_custom.py with the --ground_truth_compare option. By default, this is disabled." ] }, { diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/quantize_model.py b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/quantize_model.py index 428e24142e..e5ce7f9bbc 100644 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/quantize_model.py +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/quantize_model.py @@ -18,8 +18,6 @@ from neural_compressor.utils.pytorch import load from speechbrain.pretrained import EncoderClassifier -DEFAULT_EVAL_DATA_PATH = "/data/commonVoice/dev" - def prepare_dataset(path): data_list = [] for dir_name in os.listdir(path): @@ -33,7 +31,7 @@ def main(argv): import argparse parser = argparse.ArgumentParser() parser.add_argument('-p', type=str, required=True, help="Path to the model to be optimized") - parser.add_argument('-datapath', type=str, default=DEFAULT_EVAL_DATA_PATH, help="Path to evaluation dataset") + parser.add_argument('-datapath', type=str, required=True, help="Path to evaluation dataset") args = parser.parse_args() model_path = args.p diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/sample_input_features.pt b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/sample_input_features.pt deleted file mode 100644 index 61114fe706f7f1164a8508ee6367bc119989b427..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48939 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 E1rp{^yZ`_I diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/sample_wavs.pt b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Inference/sample_wavs.pt new file mode 100644 index 0000000000000000000000000000000000000000..72ea7cc6590ec92c73bd195f6782b1fe3741be81 GIT binary patch literal 129200 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 bJ-UDYuk*jp9&VC Hugging Face SpeechBrain +| OS | Ubuntu* 22.04 or newer +| Hardware | Intel® Xeon® and Core® processor families +| Software | Intel® AI Tools
Hugging Face SpeechBrain ## Key Implementation Details The [CommonVoice](https://commonvoice.mozilla.org/) dataset is used to train an Emphasized Channel Attention, Propagation and Aggregation Time Delay Neural Network (ECAPA-TDNN). This is implemented in the [Hugging Face SpeechBrain](https://huggingface.co/SpeechBrain) library. Additionally, a small Convolutional Recurrent Deep Neural Network (CRDNN) pretrained on the LibriParty dataset is used to process audio samples and output the segments where speech activity is detected. -After you have downloaded the CommonVoice dataset, the data must be preprocessed by converting the MP3 files into WAV format and separated into training, validation, and testing sets. - The model is then trained from scratch using the Hugging Face SpeechBrain library. This model is then used for inference on the testing dataset or a user-specified dataset. There is an option to utilize SpeechBrain's Voice Activity Detection (VAD) where only the speech segments from the audio files are extracted and combined before samples are randomly selected as input into the model. To improve performance, the user may quantize the trained model to INT8 using Intel® Neural Compressor (INC) to decrease latency. The sample contains three discreet phases: @@ -39,93 +37,94 @@ For both training and inference, you can run the sample and scripts in Jupyter N ## Prepare the Environment -### Downloading the CommonVoice Dataset +### Create and Set Up Environment ->**Note**: You can skip downloading the dataset if you already have a pretrained model and only want to run inference on custom data samples that you provide. +1. Create your conda environment by following the instructions on the Intel [AI Tools Selector](https://www.intel.com/content/www/us/en/developer/tools/oneapi/ai-tools-selector.html). You can follow these settings: -Download the CommonVoice dataset for languages of interest from [https://commonvoice.mozilla.org/en/datasets](https://commonvoice.mozilla.org/en/datasets). +* Tool: AI Tools +* Preset or customize: Customize +* Distribution Type: conda* or pip +* Python Versions: Python* 3.9 or 3.10 +* PyTorch* Framework Optimizations: Intel® Extension for PyTorch* (CPU) +* Intel®-Optimized Tools & Libraries: Intel® Neural Compressor -For this sample, you will need to download the following languages: **Japanese** and **Swedish**. Follow Steps 1-6 below or you can execute the code. +>**Note**: Be sure to activate your environment before installing the packages. If using pip, install using `python -m pip` instead of just `pip`. + +2. Create your dataset folder and set the environment variable `COMMON_VOICE_PATH`. This needs to match with where you downloaded your dataset. +```bash +mkdir -p /data/commonVoice +export COMMON_VOICE_PATH=/data/commonVoice +``` -1. On the CommonVoice website, select the Version and Language. -2. Enter your email. -3. Check the boxes, and right-click on the download button to copy the link address. -4. Paste this link into a text editor and copy the first part of the URL up to ".tar.gz". -5. Use **GNU wget** on the URL to download the data to `/data/commonVoice`. +3. Install packages needed for MP3 to WAV conversion +```bash +sudo apt-get update && apt-get install -y ffmpeg libgl1 +``` - Alternatively, you can use a directory on your local drive (due to the large amount of data). If you opt to do so, you must change the `COMMON_VOICE_PATH` environment in `launch_docker.sh` before running the script. +4. Navigate to your working directory, clone the `oneapi-src` repository, and navigate to this code sample. +```bash +git clone https://github.com/oneapi-src/oneAPI-samples.git +cd oneAPI-samples/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification +``` -6. Extract the compressed folder, and rename the folder with the language (for example, English). +5. Run the bash script to install additional necessary libraries, including SpeechBrain. +```bash +source initialize.sh +``` - The file structure **must match** the `LANGUAGE_PATHS` defined in `prepareAllCommonVoice.py` in the `Training` folder for the script to run properly. +### Download the CommonVoice Dataset -These commands illustrate Steps 1-6. Notice that it downloads Japanese and Swedish from CommonVoice version 11.0. +>**Note**: You can skip downloading the dataset if you already have a pretrained model and only want to run inference on custom data samples that you provide. + +First, change to the `Dataset` directory. ``` -# Create the commonVoice directory under 'data' -sudo chmod 777 -R /data -cd /data -mkdir commonVoice -cd commonVoice - -# Download the CommonVoice data -wget \ -https://mozilla-common-voice-datasets.s3.dualstack.us-west-2.amazonaws.com/cv-corpus-11.0-2022-09-21/cv-corpus-11.0-2022-09-21-ja.tar.gz \ -https://mozilla-common-voice-datasets.s3.dualstack.us-west-2.amazonaws.com/cv-corpus-11.0-2022-09-21/cv-corpus-11.0-2022-09-21-sv-SE.tar.gz - -# Extract and organize the CommonVoice data into respective folders by language -tar -xf cv-corpus-11.0-2022-09-21-ja.tar.gz -mv cv-corpus-11.0-2022-09-21 japanese -tar -xf cv-corpus-11.0-2022-09-21-sv-SE.tar.gz -mv cv-corpus-11.0-2022-09-21 swedish +cd ./Dataset ``` -### Configuring the Container +The `get_dataset.py` script downloads the Common Voice dataset by doing the following: -1. Pull the `oneapi-aikit` docker image. -2. Set up the Docker environment. - ``` - docker pull intel/oneapi-aikit - ./launch_docker.sh - ``` - >**Note**: By default, the `Inference` and `Training` directories will be mounted and the environment variable `COMMON_VOICE_PATH` will be set to `/data/commonVoice` and mounted to `/data`. `COMMON_VOICE_PATH` is the location of where the CommonVoice dataset is downloaded. +- Gets the train set of the [Common Voice dataset from Huggingface](https://huggingface.co/datasets/mozilla-foundation/common_voice_11_0) for Japanese and Swedish +- Downloads each mp3 and moves them to the `output_dir` folder +1. If you want to add additional languages, then modify the `language_to_code` dictionary in the file to reflect the languages to be included in the model. +3. Run the script with options. + ```bash + python get_dataset.py --output_dir ${COMMON_VOICE_PATH} + ``` + | Parameters | Description + |:--- |:--- + | `--output_dir` | Base output directory for saving the files. Default is /data/commonVoice + +Once the dataset is downloaded, navigate back to the parent directory +``` +cd .. +``` ## Train the Model with Languages This section explains how to train a model for language identification using the CommonVoice dataset, so it includes steps on how to preprocess the data, train the model, and prepare the output files for inference. -### Configure the Training Environment - -1. Change to the `Training` directory. - ``` - cd /Training - ``` -2. Source the bash script to install the necessary components. - ``` - source initialize.sh - ``` - This installs PyTorch*, the Intel® Extension for PyTorch (IPEX), and other components. +First, change to the `Training` directory. +``` +cd ./Training +``` -### Run in Jupyter Notebook +### Option 1: Run in Jupyter Notebook -1. Install Jupyter Notebook. - ``` - pip install notebook - ``` -2. Launch Jupyter Notebook. +1. Launch Jupyter Notebook. ``` jupyter notebook --ip 0.0.0.0 --port 8888 --allow-root ``` -3. Follow the instructions to open the URL with the token in your browser. -4. Locate and select the Training Notebook. +2. Follow the instructions to open the URL with the token in your browser. +3. Locate and select the Training Notebook. ``` lang_id_training.ipynb ``` -5. Follow the instructions in the Notebook. +4. Follow the instructions in the Notebook. -### Run in a Console +### Option 2: Run in a Console If you cannot or do not want to use Jupyter Notebook, use these procedures to run the sample and scripts locally. @@ -133,13 +132,13 @@ If you cannot or do not want to use Jupyter Notebook, use these procedures to ru 1. Acquire copies of the training scripts. (The command retrieves copies of the required VoxLingua107 training scripts from SpeechBrain.) ``` - cp speechbrain/recipes/VoxLingua107/lang_id/create_wds_shards.py create_wds_shards.py - cp speechbrain/recipes/VoxLingua107/lang_id/train.py train.py - cp speechbrain/recipes/VoxLingua107/lang_id/hparams/train_ecapa.yaml train_ecapa.yaml + cp ../speechbrain/recipes/VoxLingua107/lang_id/create_wds_shards.py create_wds_shards.py + cp ../speechbrain/recipes/VoxLingua107/lang_id/train.py train.py + cp ../speechbrain/recipes/VoxLingua107/lang_id/hparams/train_ecapa.yaml train_ecapa.yaml ``` 2. From the `Training` directory, apply patches to modify these files to work with the CommonVoice dataset. - ``` + ```bash patch < create_wds_shards.patch patch < train_ecapa.patch ``` @@ -154,8 +153,8 @@ The `prepareAllCommonVoice.py` script performs the following data preprocessing 1. If you want to add additional languages, then modify the `LANGUAGE_PATHS` list in the file to reflect the languages to be included in the model. 2. Run the script with options. The samples will be divided as follows: 80% training, 10% validation, 10% testing. - ``` - python prepareAllCommonVoice.py -path /data -max_samples 2000 --createCsv --train --dev --test + ```bash + python prepareAllCommonVoice.py -path $COMMON_VOICE_PATH -max_samples 2000 --createCsv --train --dev --test ``` | Parameters | Description |:--- |:--- @@ -166,27 +165,28 @@ The `prepareAllCommonVoice.py` script performs the following data preprocessing #### Create Shards for Training and Validation -1. If the `/data/commonVoice_shards` folder exists, delete the folder and the contents before proceeding. +1. If the `${COMMON_VOICE_PATH}/processed_data/commonVoice_shards` folder exists, delete the folder and the contents before proceeding. 2. Enter the following commands. + ```bash + python create_wds_shards.py ${COMMON_VOICE_PATH}/processed_data/train ${COMMON_VOICE_PATH}/processed_data/commonVoice_shards/train + python create_wds_shards.py ${COMMON_VOICE_PATH}/processed_data/dev ${COMMON_VOICE_PATH}/processed_data/commonVoice_shards/dev ``` - python create_wds_shards.py /data/commonVoice/train/ /data/commonVoice_shards/train - python create_wds_shards.py /data/commonVoice/dev/ /data/commonVoice_shards/dev - ``` -3. Note the shard with the largest number as `LARGEST_SHARD_NUMBER` in the output above or by navigating to `/data/commonVoice_shards/train`. +3. Note the shard with the largest number as `LARGEST_SHARD_NUMBER` in the output above or by navigating to `${COMMON_VOICE_PATH}/processed_data/commonVoice_shards/train`. 4. Open the `train_ecapa.yaml` file and modify the `train_shards` variable to make the range reflect: `000000..LARGEST_SHARD_NUMBER`. -5. Repeat the process for `/data/commonVoice_shards/dev`. +5. Repeat Steps 3 and 4 for `${COMMON_VOICE_PATH}/processed_data/commonVoice_shards/dev`. #### Run the Training Script -The YAML file `train_ecapa.yaml` with the training configurations should already be patched from the Prerequisite section. +The YAML file `train_ecapa.yaml` with the training configurations is passed as an argument to the `train.py` script to train the model. 1. If necessary, edit the `train_ecapa.yaml` file to meet your needs. | Parameters | Description |:--- |:--- + | `seed` | The seed value, which should be set to a different value for subsequent runs. Defaults to 1987. | `out_n_neurons` | Must be equal to the number of languages of interest. | `number_of_epochs` | Default is **10**. Adjust as needed. - | `batch_size` | In the trainloader_options, decrease this value if your CPU or GPU runs out of memory while running the training script. + | `batch_size` | In the trainloader_options, decrease this value if your CPU or GPU runs out of memory while running the training script. If you see a "Killed" error message, then the training script has run out of memory. 2. Run the script to train the model. ``` @@ -195,30 +195,48 @@ The YAML file `train_ecapa.yaml` with the training configurations should already #### Move Model to Inference Folder -After training, the output should be inside `results/epaca/SEED_VALUE` folder. By default SEED_VALUE is set to 1987 in the YAML file. You can change the value as needed. +After training, the output should be inside the `results/epaca/1987` folder. By default the `seed` is set to 1987 in `train_ecapa.yaml`. You can change the value as needed. -1. Copy all files with *cp -R* from `results/epaca/SEED_VALUE` into a new folder called `lang_id_commonvoice_model` in the **Inference** folder. - - The name of the folder MUST match with the pretrained_path variable defined in the YAML file. By default, it is `lang_id_commonvoice_model`. +1. Copy all files from `results/epaca/1987` into a new folder called `lang_id_commonvoice_model` in the **Inference** folder. + ```bash + cp -R results/epaca/1987 ../Inference/lang_id_commonvoice_model + ``` + The name of the folder MUST match with the pretrained_path variable defined in `train_ecapa.yaml`. By default, it is `lang_id_commonvoice_model`. 2. Change directory to `/Inference/lang_id_commonvoice_model/save`. + ```bash + cd ../Inference/lang_id_commonvoice_model/save + ``` + 3. Copy the `label_encoder.txt` file up one level. -4. Change to the latest `CKPT` folder, and copy the classifier.ckpt and embedding_model.ckpt files into the `/Inference/lang_id_commonvoice_model/` folder. + ```bash + cp label_encoder.txt ../. + ``` + +4. Change to the latest `CKPT` folder, and copy the classifier.ckpt and embedding_model.ckpt files into the `/Inference/lang_id_commonvoice_model/` folder which is two directories up. By default, the command below will navigate into the single CKPT folder that is present, but you can change it to the specific folder name. + ```bash + # Navigate into the CKPT folder + cd CKPT* + + cp classifier.ckpt ../../. + cp embedding_model.ckpt ../../ + cd ../../../.. + ``` - You may need to modify the permissions of these files to be executable before you run the inference scripts to consume them. + You may need to modify the permissions of these files to be executable i.e. `sudo chmod 755` before you run the inference scripts to consume them. >**Note**: If `train.py` is rerun with the same seed, it will resume from the epoch number it last run. For a clean rerun, delete the `results` folder or change the seed. You can now load the model for inference. In the `Inference` folder, the `inference_commonVoice.py` script uses the trained model on the testing dataset, whereas `inference_custom.py` uses the trained model on a user-specified dataset and can utilize Voice Activity Detection. ->**Note**: If the folder name containing the model is changed from `lang_id_commonvoice_model`, you will need to modify the `source_model_path` variable in `inference_commonVoice.py` and `inference_custom.py` files in the `speechbrain_inference` class. +>**Note**: If the folder name containing the model is changed from `lang_id_commonvoice_model`, you will need to modify the `pretrained_path` in `train_ecapa.yaml`, and the `source_model_path` variable in both the `inference_commonVoice.py` and `inference_custom.py` files in the `speechbrain_inference` class. ## Run Inference for Language Identification >**Stop**: If you have not already done so, you must run the scripts in the `Training` folder to generate the trained model before proceeding. -To run inference, you must have already run all of the training scripts, generated the trained model, and moved files to the appropriate locations. You must place the model output in a folder name matching the name specified as the `pretrained_path` variable defined in the YAML file. +To run inference, you must have already run all of the training scripts, generated the trained model, and moved files to the appropriate locations. You must place the model output in a folder name matching the name specified as the `pretrained_path` variable defined in `train_ecapa.yaml`. >**Note**: If you plan to run inference on **custom data**, you will need to create a folder for the **.wav** files to be used for prediction. For example, `data_custom`. Move the **.wav** files to your custom folder. (For quick results, you may select a few audio files from each language downloaded from CommonVoice.) @@ -226,35 +244,23 @@ To run inference, you must have already run all of the training scripts, generat 1. Change to the `Inference` directory. ``` - cd /Inference - ``` -2. Source the bash script to install or update the necessary components. - ``` - source initialize.sh - ``` -3. Patch the Intel® Extension for PyTorch (IPEX) to use SpeechBrain models. (This patch is required for PyTorch* TorchScript to work because the output of the model must contain only tensors.) - ``` - patch ./speechbrain/speechbrain/pretrained/interfaces.py < interfaces.patch + cd ./Inference ``` -### Run in Jupyter Notebook +### Option 1: Run in Jupyter Notebook -1. If you have not already done so, install Jupyter Notebook. - ``` - pip install notebook - ``` -2. Launch Jupyter Notebook. +1. Launch Jupyter Notebook. ``` - jupyter notebook --ip 0.0.0.0 --port 8888 --allow-root + jupyter notebook --ip 0.0.0.0 --port 8889 --allow-root ``` -3. Follow the instructions to open the URL with the token in your browser. -4. Locate and select the inference Notebook. +2. Follow the instructions to open the URL with the token in your browser. +3. Locate and select the inference Notebook. ``` lang_id_inference.ipynb ``` -5. Follow the instructions in the Notebook. +4. Follow the instructions in the Notebook. -### Run in a Console +### Option 2: Run in a Console If you cannot or do not want to use Jupyter Notebook, use these procedures to run the sample and scripts locally. @@ -287,34 +293,32 @@ Both scripts support input options; however, some options can be use on `inferen #### On the CommonVoice Dataset 1. Run the inference_commonvoice.py script. - ``` - python inference_commonVoice.py -p /data/commonVoice/test + ```bash + python inference_commonVoice.py -p ${COMMON_VOICE_PATH}/processed_data/test ``` The script should create a `test_data_accuracy.csv` file that summarizes the results. #### On Custom Data -1. Modify the `audio_ground_truth_labels.csv` file to include the name of the audio file and expected audio label (like, `en` for English). +To run inference on custom data, you must specify a folder with **.wav** files and pass the path in as an argument. You can do so by creating a folder named `data_custom` and then copy 1 or 2 **.wav** files from your test dataset into it. **.mp3** files will NOT work. - By default, this is disabled. If required, use the `--ground_truth_compare` input option. To run inference on custom data, you must specify a folder with **.wav** files and pass the path in as an argument. - -2. Run the inference_ script. - ``` - python inference_custom.py -p - ``` +Run the inference_ script. +```bash +python inference_custom.py -p +``` The following examples describe how to use the scripts to produce specific outcomes. **Default: Random Selections** 1. To randomly select audio clips from audio files for prediction, enter commands similar to the following: - ``` + ```bash python inference_custom.py -p data_custom -d 3 -s 50 ``` This picks 50 3-second samples from each **.wav** file in the `data_custom` folder. The `output_summary.csv` file summarizes the results. 2. To randomly select audio clips from audio files after applying **Voice Activity Detection (VAD)**, use the `--vad` option: - ``` + ```bash python inference_custom.py -p data_custom -d 3 -s 50 --vad ``` Again, the `output_summary.csv` file summarizes the results. @@ -324,18 +328,20 @@ The following examples describe how to use the scripts to produce specific outco **Optimization with Intel® Extension for PyTorch (IPEX)** 1. To optimize user-defined data, enter commands similar to the following: - ``` + ```bash python inference_custom.py -p data_custom -d 3 -s 50 --vad --ipex --verbose ``` + This will apply `ipex.optimize` to the model(s) and TorchScript. You can also add the `--bf16` option along with `--ipex` to run in the BF16 data type, supported on 4th Gen Intel® Xeon® Scalable processors and newer. + >**Note**: The `--verbose` option is required to view the latency measurements. **Quantization with Intel® Neural Compressor (INC)** 1. To improve inference latency, you can use the Intel® Neural Compressor (INC) to quantize the trained model from FP32 to INT8 by running `quantize_model.py`. + ```bash + python quantize_model.py -p ./lang_id_commonvoice_model -datapath $COMMON_VOICE_PATH/processed_data/dev ``` - python quantize_model.py -p ./lang_id_commonvoice_model -datapath $COMMON_VOICE_PATH/dev - ``` - Use the `-datapath` argument to specify a custom evaluation dataset. By default, the datapath is set to the `/data/commonVoice/dev` folder that was generated from the data preprocessing scripts in the `Training` folder. + Use the `-datapath` argument to specify a custom evaluation dataset. By default, the datapath is set to the `$COMMON_VOICE_PATH/processed_data/dev` folder that was generated from the data preprocessing scripts in the `Training` folder. After quantization, the model will be stored in `lang_id_commonvoice_model_INT8` and `neural_compressor.utils.pytorch.load` will have to be used to load the quantized model for inference. If `self.language_id` is the original model and `data_path` is the path to the audio file: ``` @@ -345,9 +351,16 @@ The following examples describe how to use the scripts to produce specific outco prediction = self.model_int8(signal) ``` -### Troubleshooting + The code above is integrated into `inference_custom.py`. You can now run inference on your data using this INT8 model: + ```bash + python inference_custom.py -p data_custom -d 3 -s 50 --vad --int8_model --verbose + ``` + + >**Note**: The `--verbose` option is required to view the latency measurements. + +**(Optional) Comparing Predictions with Ground Truth** -If the model appears to be giving the same output regardless of input, try running `clean.sh` to remove the `RIR_NOISES` and `speechbrain` folders. Redownload that data after cleaning by running `initialize.sh` and either `inference_commonVoice.py` or `inference_custom.py`. +You can choose to modify `audio_ground_truth_labels.csv` to include the name of the audio file and expected audio label (like, `en` for English), then run `inference_custom.py` with the `--ground_truth_compare` option. By default, this is disabled. ## License diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/clean.sh b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/clean.sh index f60b245773..30f1806c10 100644 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/clean.sh +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/clean.sh @@ -1,5 +1,4 @@ #!/bin/bash -rm -R RIRS_NOISES -rm -R speechbrain -rm -f rirs_noises.zip noise.csv reverb.csv +echo "Deleting rir, noise, speechbrain" +rm -R rir noise diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/create_wds_shards.patch b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/create_wds_shards.patch index ddfe37588b..3d60bc627f 100644 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/create_wds_shards.patch +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/create_wds_shards.patch @@ -1,5 +1,5 @@ ---- create_wds_shards.py 2022-09-20 14:55:48.732386718 -0700 -+++ create_wds_shards_commonvoice.py 2022-09-20 14:53:56.554637629 -0700 +--- create_wds_shards.py 2024-11-13 18:08:07.440000000 -0800 ++++ create_wds_shards_modified.py 2024-11-14 14:09:36.225000000 -0800 @@ -27,7 +27,10 @@ t, sr = torchaudio.load(audio_file_path) @@ -12,7 +12,7 @@ return t -@@ -61,27 +64,20 @@ +@@ -66,27 +69,22 @@ sample_keys_per_language = defaultdict(list) for f in audio_files: @@ -23,7 +23,9 @@ - f.as_posix(), - ) + # Common Voice format -+ # commonVoice_folder_path/common_voice__00000000.wav' ++ # commonVoice_folder_path/processed_data//common_voice__00000000.wav' ++ # DATASET_TYPE: dev, test, train ++ # LANG_ID: the label for the language + m = re.match(r"((.*)(common_voice_)(.+)(_)(\d+).wav)", f.as_posix()) + if m: diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/initialize.sh b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/initialize.sh deleted file mode 100644 index 78c114f2dc..0000000000 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/initialize.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -# Activate the oneAPI environment for PyTorch -source activate pytorch - -# Install speechbrain -git clone https://github.com/speechbrain/speechbrain.git -cd speechbrain -pip install -r requirements.txt -pip install --editable . -cd .. - -# Add speechbrain to environment variable PYTHONPATH -export PYTHONPATH=$PYTHONPATH:/Training/speechbrain - -# Install webdataset -pip install webdataset==0.1.96 - -# Install PyTorch and Intel Extension for PyTorch (IPEX) -pip install torch==1.13.1 torchaudio -pip install --no-deps torchvision==0.14.0 -pip install intel_extension_for_pytorch==1.13.100 - -# Install libraries for MP3 to WAV conversion -pip install pydub -apt-get update && apt-get install ffmpeg diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/lang_id_training.ipynb b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/lang_id_training.ipynb index 0502d223e9..4550b88916 100644 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/lang_id_training.ipynb +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/lang_id_training.ipynb @@ -29,9 +29,9 @@ "metadata": {}, "outputs": [], "source": [ - "!cp speechbrain/recipes/VoxLingua107/lang_id/create_wds_shards.py create_wds_shards.py\n", - "!cp speechbrain/recipes/VoxLingua107/lang_id/train.py train.py\n", - "!cp speechbrain/recipes/VoxLingua107/lang_id/hparams/train_ecapa.yaml train_ecapa.yaml" + "!cp ../speechbrain/recipes/VoxLingua107/lang_id/create_wds_shards.py create_wds_shards.py\n", + "!cp ../speechbrain/recipes/VoxLingua107/lang_id/train.py train.py\n", + "!cp ../speechbrain/recipes/VoxLingua107/lang_id/hparams/train_ecapa.yaml train_ecapa.yaml" ] }, { @@ -75,7 +75,7 @@ "metadata": {}, "outputs": [], "source": [ - "!python prepareAllCommonVoice.py -path /data -max_samples 2000 --createCsv --train --dev --test" + "!python prepareAllCommonVoice.py -path $COMMON_VOICE_PATH -max_samples 2000 --createCsv --train --dev --test" ] }, { @@ -102,15 +102,15 @@ "metadata": {}, "outputs": [], "source": [ - "!python create_wds_shards.py /data/commonVoice/train/ /data/commonVoice_shards/train \n", - "!python create_wds_shards.py /data/commonVoice/dev/ /data/commonVoice_shards/dev" + "!python create_wds_shards.py ${COMMON_VOICE_PATH}/processed_data/train ${COMMON_VOICE_PATH}/processed_data/commonVoice_shards/train \n", + "!python create_wds_shards.py ${COMMON_VOICE_PATH}/processed_data/dev ${COMMON_VOICE_PATH}/processed_data/commonVoice_shards/dev" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Note down the shard with the largest number as LARGEST_SHARD_NUMBER in the output above or by navigating to */data/commonVoice_shards/train*. In *train_ecapa.yaml*, modify the *train_shards* variable to go from 000000..LARGEST_SHARD_NUMBER. Repeat the process for */data/commonVoice_shards/dev*. " + "Note down the shard with the largest number as LARGEST_SHARD_NUMBER in the output above or by navigating to `${COMMON_VOICE_PATH}/processed_data/commonVoice_shards/train`. In `train_ecapa.yaml`, modify the `train_shards` variable to go from 000000..LARGEST_SHARD_NUMBER. Repeat the process for `${COMMON_VOICE_PATH}/processed_data/commonVoice_shards/dev`. " ] }, { @@ -126,6 +126,7 @@ "source": [ "### Run the training script \n", "The YAML file *train_ecapa.yaml* with the training configurations should already be patched from the Prerequisite section. The following parameters can be adjusted in the file directly as needed: \n", + "* *seed* should be set to a different value for subsequent runs. Defaults to 1987\n", "* *out_n_neurons* must be equal to the number of languages of interest \n", "* *number_of_epochs* is set to 10 by default but can be adjusted \n", "* In the trainloader_options, the *batch_size* may need to be decreased if your CPU or GPU runs out of memory while running the training script. \n", @@ -147,18 +148,57 @@ "metadata": {}, "source": [ "### Move output model to Inference folder \n", - "After training, the output should be inside results/epaca/SEED_VALUE. By default SEED_VALUE is set to 1987 in the YAML file. This value can be changed. Follow these instructions next: \n", + "After training, the output should be inside the `results/epaca/1987` folder. By default the `seed` is set to 1987 in `train_ecapa.yaml`. You can change the value as needed.\n", "\n", - "1. Copy all files with *cp -R* from results/epaca/SEED_VALUE into a new folder called *lang_id_commonvoice_model* in the Inference folder. The name of the folder MUST match with the pretrained_path variable defined in the YAML file. By default, it is *lang_id_commonvoice_model*. \n", - "2. Navigate to /Inference/land_id_commonvoice_model/save. \n", - "3. Copy the label_encoder.txt file up one level. \n", - "4. Navigate into the latest CKPT folder and copy the classifier.ckpt and embedding_model.ckpt files into the /Inference/lang_id_commonvoice_model/ level. You may need to modify the permissions of these files to be executable before you run the inference scripts to consume them. \n", + "1. Copy all files from `results/epaca/1987` into a new folder called `lang_id_commonvoice_model` in the **Inference** folder.\n", + " The name of the folder MUST match with the pretrained_path variable defined in `train_ecapa.yaml`. By default, it is `lang_id_commonvoice_model`.\n", "\n", - "Note that if *train.py* is rerun with the same seed, it will resume from the epoch number it left off of. For a clean rerun, delete the *results* folder or change the seed. \n", + "2. Change directory to `/Inference/lang_id_commonvoice_model/save`.\n", "\n", - "### Running inference\n", - "At this point, the model can be loaded and used in inference. In the Inference folder, inference_commonVoice.py uses the trained model on \n", - "the testing dataset, whereas inference_custom.py uses the trained model on a user-specified dataset and utilizes Voice Activity Detection. Note that if the folder name containing the model is changed from *lang_id_commonvoice_model*, you will need to modify inference_commonVoice.py and inference_custom.py's *source_model_path* variable in the *speechbrain_inference* class. " + "3. Copy the `label_encoder.txt` file up one level.\n", + "\n", + "4. Change to the latest `CKPT` folder, and copy the classifier.ckpt and embedding_model.ckpt files into the `/Inference/lang_id_commonvoice_model/` folder which is two directories up." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "# 1)\n", + "!cp -R results/epaca/1987 ../Inference/lang_id_commonvoice_model\n", + "\n", + "# 2)\n", + "os.chdir(\"../Inference/lang_id_commonvoice_model/save\")\n", + "\n", + "# 3)\n", + "!cp label_encoder.txt ../.\n", + "\n", + "# 4) \n", + "folders = os.listdir()\n", + "for folder in folders:\n", + " if \"CKPT\" in folder:\n", + " os.chdir(folder)\n", + " break\n", + "!cp classifier.ckpt ../../.\n", + "!cp embedding_model.ckpt ../../\n", + "os.chdir(\"../../../..\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You may need to modify the permissions of these files to be executable i.e. `sudo chmod 755` before you run the inference scripts to consume them.\n", + "\n", + ">**Note**: If `train.py` is rerun with the same seed, it will resume from the epoch number it last run. For a clean rerun, delete the `results` folder or change the seed.\n", + "\n", + "You can now load the model for inference. In the `Inference` folder, the `inference_commonVoice.py` script uses the trained model on the testing dataset, whereas `inference_custom.py` uses the trained model on a user-specified dataset and can utilize Voice Activity Detection. \n", + "\n", + ">**Note**: If the folder name containing the model is changed from `lang_id_commonvoice_model`, you will need to modify the `pretrained_path` in `train_ecapa.yaml`, and the `source_model_path` variable in both the `inference_commonVoice.py` and `inference_custom.py` files in the `speechbrain_inference` class. " ] } ], diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/prepareAllCommonVoice.py b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/prepareAllCommonVoice.py index ed78ab5c35..a6ab8df1b2 100644 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/prepareAllCommonVoice.py +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/prepareAllCommonVoice.py @@ -124,9 +124,9 @@ def main(argv): createCsv = args.createCsv # Data paths - TRAIN_PATH = commonVoicePath + "/commonVoice/train" - TEST_PATH = commonVoicePath + "/commonVoice/test" - DEV_PATH = commonVoicePath + "/commonVoice/dev" + TRAIN_PATH = commonVoicePath + "/processed_data/train" + TEST_PATH = commonVoicePath + "/processed_data/test" + DEV_PATH = commonVoicePath + "/processed_data/dev" # Prepare the csv files for the Common Voice dataset if createCsv: diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/train_ecapa.patch b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/train_ecapa.patch index 38db22cf39..c95bf540ad 100644 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/train_ecapa.patch +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/Training/train_ecapa.patch @@ -1,60 +1,55 @@ ---- train_ecapa.yaml.orig 2023-02-09 17:17:34.849537612 +0000 -+++ train_ecapa.yaml 2023-02-09 17:19:42.936542193 +0000 -@@ -4,19 +4,19 @@ +--- train_ecapa.yaml 2024-11-13 18:08:40.313000000 -0800 ++++ train_ecapa_modified.yaml 2024-11-14 14:52:31.232000000 -0800 +@@ -4,17 +4,17 @@ # ################################ # Basic parameters -seed: 1988 +seed: 1987 - __set_seed: !apply:torch.manual_seed [!ref ] + __set_seed: !apply:speechbrain.utils.seed_everything [!ref ] output_folder: !ref results/epaca/ save_folder: !ref /save train_log: !ref /train_log.txt -data_folder: !PLACEHOLDER +data_folder: ./ - rir_folder: !ref - # skip_prep: False -shards_url: /data/voxlingua107_shards -+shards_url: /data/commonVoice_shards ++shards_url: /data/commonVoice/processed_data/commonVoice_shards train_meta: !ref /train/meta.json val_meta: !ref /dev/meta.json -train_shards: !ref /train/shard-{000000..000507}.tar +train_shards: !ref /train/shard-{000000..000000}.tar val_shards: !ref /dev/shard-000000.tar - # Set to directory on a large disk if you are training on Webdataset shards hosted on the web -@@ -25,7 +25,7 @@ + # Data for augmentation +@@ -32,7 +32,7 @@ ckpt_interval_minutes: 5 # Training parameters -number_of_epochs: 40 -+number_of_epochs: 10 ++number_of_epochs: 3 lr: 0.001 lr_final: 0.0001 sample_rate: 16000 -@@ -38,11 +38,11 @@ +@@ -45,10 +45,10 @@ deltas: False # Number of languages -out_n_neurons: 107 +out_n_neurons: 2 +-num_workers: 4 +-batch_size: 128 ++num_workers: 1 ++batch_size: 64 + batch_size_val: 32 train_dataloader_options: -- num_workers: 4 -- batch_size: 128 -+ num_workers: 1 -+ batch_size: 64 + num_workers: !ref +@@ -60,6 +60,21 @@ - val_dataloader_options: - num_workers: 1 -@@ -138,3 +138,20 @@ - classifier: !ref - normalizer: !ref - counter: !ref -+ -+# Below most relevant for inference using self-trained model: -+ + ############################## Augmentations ################################### + ++# Changes for code sample to work with CommonVoice dataset +pretrained_path: lang_id_commonvoice_model + +label_encoder: !new:speechbrain.dataio.encoder.CategoricalEncoder @@ -69,3 +64,6 @@ + classifier: !ref /classifier.ckpt + label_encoder: !ref /label_encoder.txt + + # Download and prepare the dataset of noisy sequences for augmentation + prepare_noise_data: !name:speechbrain.augment.preparation.prepare_dataset_from_URL + URL: !ref diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/initialize.sh b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/initialize.sh new file mode 100644 index 0000000000..0021b588b1 --- /dev/null +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/initialize.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Install huggingface datasets and other requirements +conda install -c conda-forge -y datasets tqdm librosa jupyter ipykernel ipywidgets + +# Install speechbrain +git clone --depth 1 --branch v1.0.2 https://github.com/speechbrain/speechbrain.git +cd speechbrain +python -m pip install -r requirements.txt +python -m pip install --editable . +cd .. + +# Add speechbrain to environment variable PYTHONPATH +export PYTHONPATH=$PYTHONPATH:$(pwd)/speechbrain + +# Install webdataset +python -m pip install webdataset==0.2.100 + +# Install libraries for MP3 to WAV conversion +python -m pip install pydub + +# Install notebook to run Jupyter notebooks +python -m pip install notebook diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/launch_docker.sh b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/launch_docker.sh deleted file mode 100644 index 546523f6f6..0000000000 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/launch_docker.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -export COMMON_VOICE_PATH="/data/commonVoice" -export DOCKER_RUN_ENVS="-e ftp_proxy=${ftp_proxy} -e FTP_PROXY=${FTP_PROXY} -e http_proxy=${http_proxy} -e HTTP_PROXY=${HTTP_PROXY} -e https_proxy=${https_proxy} -e HTTPS_PROXY=${HTTPS_PROXY} -e no_proxy=${no_proxy} -e NO_PROXY=${NO_PROXY} -e socks_proxy=${socks_proxy} -e SOCKS_PROXY=${SOCKS_PROXY} -e COMMON_VOICE_PATH=${COMMON_VOICE_PATH}" -docker run --privileged ${DOCKER_RUN_ENVS} -it --rm --network host \ - -v"/home:/home" \ - -v"/tmp:/tmp" \ - -v "${PWD}/Inference":/Inference \ - -v "${PWD}/Training":/Training \ - -v "${COMMON_VOICE_PATH}":/data \ - --shm-size 32G \ - intel/oneapi-aikit - \ No newline at end of file diff --git a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/sample.json b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/sample.json index ba157302ff..768ed8eb6d 100644 --- a/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/sample.json +++ b/AI-and-Analytics/End-to-end-Workloads/LanguageIdentification/sample.json @@ -12,8 +12,19 @@ { "id": "Language_Identification_E2E", "env": [ + "export COMMON_VOICE_PATH=/data/commonVoice" ], "steps": [ + "mkdir -p /data/commonVoice", + "apt-get update && apt-get install ffmpeg libgl1 -y", + "source initialize.sh", + "cd ./Dataset", + "python get_dataset.py --output_dir ${COMMON_VOICE_PATH}", + "cd ..", + "cd ./Training", + "jupyter nbconvert --execute --to notebook --inplace --debug lang_id_training.ipynb", + "cd ./Inference", + "jupyter nbconvert --execute --to notebook --inplace --debug lang_id_inference.ipynb" ] } ] diff --git a/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/.gitkeep b/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/License.txt b/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/License.txt new file mode 100644 index 0000000000..e63c6e13dc --- /dev/null +++ b/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/License.txt @@ -0,0 +1,7 @@ +Copyright Intel Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/README.md b/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/README.md new file mode 100644 index 0000000000..a8fb984dd9 --- /dev/null +++ b/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/README.md @@ -0,0 +1,140 @@ +# `JAX Getting Started` Sample + +The `JAX Getting Started` sample demonstrates how to train a JAX model and run inference on Intel® hardware. +| Property | Description +|:--- |:--- +| Category | Get Start Sample +| What you will learn | How to start using JAX* on Intel® hardware. +| Time to complete | 10 minutes + +## Purpose + +JAX is a high-performance numerical computing library that enables automatic differentiation. It provides features like just-in-time compilation and efficient parallelization for machine learning and scientific computing tasks. + +This sample code shows how to get started with JAX on CPU. The sample code defines a simple neural network that trains on the MNIST dataset using JAX for parallel computations across multiple CPU cores. The network trains over multiple epochs, evaluates accuracy, and adjusts parameters using stochastic gradient descent across devices. + +## Prerequisites + +| Optimized for | Description +|:--- |:--- +| OS | Ubuntu* 22.0.4 and newer +| Hardware | Intel® Xeon® Scalable processor family +| Software | JAX + +> **Note**: AI and Analytics samples are validated on AI Tools Offline Installer. For the full list of validated platforms refer to [Platform Validation](https://github.com/oneapi-src/oneAPI-samples/tree/master?tab=readme-ov-file#platform-validation). + +## Key Implementation Details + +The getting-started sample code uses the python file 'spmd_mnist_classifier_fromscratch.py' under the examples directory in the +[jax repository](https://github.com/google/jax/). +It implements a simple neural network's training and inference for mnist images. The images are downloaded to a temporary directory when the example is run first. +- **init_random_params** initializes the neural network weights and biases for each layer. +- **predict** computes the forward pass of the network, applying weights, biases, and activations to inputs. +- **loss** calculates the cross-entropy loss between predictions and target labels. +- **spmd_update** performs parallel gradient updates across multiple devices using JAX’s pmap and lax.psum. +- **accuracy** computes the accuracy of the model by predicting the class of each input in the batch and comparing it to the true target class. It uses the *jnp.argmax* function to find the predicted class and then computes the mean of correct predictions. +- **data_stream** function generates batches of shuffled training data. It reshapes the data so that it can be split across multiple cores, ensuring that the batch size is divisible by the number of cores for parallel processing. +- **training loop** trains the model for a set number of epochs, updating parameters and printing training/test accuracy after each epoch. The parameters are replicated across devices and updated in parallel using spmd_update. After each epoch, the model’s accuracy is evaluated on both training and test data using accuracy. + +## Environment Setup + +You will need to download and install the following toolkits, tools, and components to use the sample. + +**1. Get Intel® AI Tools** + +Required AI Tools: 'JAX' +
If you have not already, select and install these Tools via [AI Tools Selector](https://www.intel.com/content/www/us/en/developer/tools/oneapi/ai-tools-selector.html). AI and Analytics samples are validated on AI Tools Offline Installer. It is recommended to select Offline Installer option in AI Tools Selector.
+please see the [supported versions](https://www.intel.com/content/www/us/en/developer/tools/oneapi/ai-tools-selector.html). + +>**Note**: If Docker option is chosen in AI Tools Selector, refer to [Working with Preset Containers](https://github.com/intel/ai-containers/tree/main/preset) to learn how to run the docker and samples. + +**2. (Offline Installer) Activate the AI Tools bundle base environment** + +If the default path is used during the installation of AI Tools: +``` +source $HOME/intel/oneapi/intelpython/bin/activate +``` +If a non-default path is used: +``` +source /bin/activate +``` + +**3. (Offline Installer) Activate relevant Conda environment** + +For the system with Intel CPU: +``` +conda activate jax +``` + +**4. Clone the GitHub repository** +``` +git clone https://github.com/google/jax.git +cd jax +export PYTHONPATH=$PYTHONPATH:$(pwd) +``` +## Run the Sample + +>**Note**: Before running the sample, make sure Environment Setup is completed. +Go to the section which corresponds to the installation method chosen in [AI Tools Selector](https://www.intel.com/content/www/us/en/developer/tools/oneapi/ai-tools-selector.html) to see relevant instructions: +* [AI Tools Offline Installer (Validated)/Conda/PIP](#ai-tools-offline-installer-validatedcondapip) +* [Docker](#docker) +### AI Tools Offline Installer (Validated)/Conda/PIP +``` + python examples/spmd_mnist_classifier_fromscratch.py +``` +### Docker +AI Tools Docker images already have Get Started samples pre-installed. Refer to [Working with Preset Containers](https://github.com/intel/ai-containers/tree/main/preset) to learn how to run the docker and samples. +## Example Output +1. When the program is run, you should see results similar to the following: + +``` +downloaded https://storage.googleapis.com/cvdf-datasets/mnist/train-images-idx3-ubyte.gz to /tmp/jax_example_data/ +downloaded https://storage.googleapis.com/cvdf-datasets/mnist/train-labels-idx1-ubyte.gz to /tmp/jax_example_data/ +downloaded https://storage.googleapis.com/cvdf-datasets/mnist/t10k-images-idx3-ubyte.gz to /tmp/jax_example_data/ +downloaded https://storage.googleapis.com/cvdf-datasets/mnist/t10k-labels-idx1-ubyte.gz to /tmp/jax_example_data/ +Epoch 0 in 2.71 sec +Training set accuracy 0.7381166815757751 +Test set accuracy 0.7516999840736389 +Epoch 1 in 2.35 sec +Training set accuracy 0.81454998254776 +Test set accuracy 0.8277999758720398 +Epoch 2 in 2.33 sec +Training set accuracy 0.8448166847229004 +Test set accuracy 0.8568999767303467 +Epoch 3 in 2.34 sec +Training set accuracy 0.8626833558082581 +Test set accuracy 0.8715999722480774 +Epoch 4 in 2.30 sec +Training set accuracy 0.8752999901771545 +Test set accuracy 0.8816999793052673 +Epoch 5 in 2.33 sec +Training set accuracy 0.8839333653450012 +Test set accuracy 0.8899999856948853 +Epoch 6 in 2.37 sec +Training set accuracy 0.8908833265304565 +Test set accuracy 0.8944999575614929 +Epoch 7 in 2.31 sec +Training set accuracy 0.8964999914169312 +Test set accuracy 0.8986999988555908 +Epoch 8 in 2.28 sec +Training set accuracy 0.9016000032424927 +Test set accuracy 0.9034000039100647 +Epoch 9 in 2.31 sec +Training set accuracy 0.9060333371162415 +Test set accuracy 0.9059999585151672 +``` + +2. Troubleshooting + + If you receive an error message, troubleshoot the problem using the **Diagnostics Utility for Intel® oneAPI Toolkits**. The diagnostic utility provides configuration and system checks to help find missing dependencies, permissions errors, and other issues. See the *[Diagnostics Utility for Intel® oneAPI Toolkits User Guide](https://www.intel.com/content/www/us/en/develop/documentation/diagnostic-utility-user-guide/top.html)* for more information on using the utility + +## License + +Code samples are licensed under the MIT license. See +[License.txt](https://github.com/oneapi-src/oneAPI-samples/blob/master/License.txt) +for details. + +Third party program Licenses can be found here: +[third-party-programs.txt](https://github.com/oneapi-src/oneAPI-samples/blob/master/third-party-programs.txt) + +*Other names and brands may be claimed as the property of others. [Trademarks](https://www.intel.com/content/www/us/en/legal/trademarks.html) diff --git a/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/run.sh b/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/run.sh new file mode 100644 index 0000000000..2a8313d002 --- /dev/null +++ b/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/run.sh @@ -0,0 +1,6 @@ +source $HOME/intel/oneapi/intelpython/bin/activate +conda activate jax +git clone https://github.com/google/jax.git +cd jax +export PYTHONPATH=$PYTHONPATH:$(pwd) +python examples/spmd_mnist_classifier_fromscratch.py diff --git a/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/sample.json b/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/sample.json new file mode 100644 index 0000000000..96c1fffd5b --- /dev/null +++ b/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/sample.json @@ -0,0 +1,24 @@ +{ + "guid": "9A6A140B-FBD0-4CB2-849A-9CAF15A6F3B1", + "name": "Getting Started example for JAX CPU", + "categories": ["Toolkit/oneAPI AI And Analytics/Getting Started"], + "description": "This sample illustrates how to train a JAX model and run inference", + "builder": ["cli"], + "languages": [{ + "python": {} + }], + "os": ["linux"], + "targetDevice": ["CPU"], + "ciTests": { + "linux": [{ + "id": "JAX CPU example", + "steps": [ + "git clone https://github.com/google/jax.git", + "cd jax", + "conda activate jax", + "python examples/spmd_mnist_classifier_fromscratch.py" + ] + }] + }, + "expertise": "Getting Started" +} diff --git a/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/third-party-programs.txt b/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/third-party-programs.txt new file mode 100644 index 0000000000..e9f8042d0a --- /dev/null +++ b/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted/third-party-programs.txt @@ -0,0 +1,253 @@ +oneAPI Code Samples - Third Party Programs File + +This file contains the list of third party software ("third party programs") +contained in the Intel software and their required notices and/or license +terms. This third party software, even if included with the distribution of the +Intel software, may be governed by separate license terms, including without +limitation, third party license terms, other Intel software license terms, and +open source software license terms. These separate license terms govern your use +of the third party programs as set forth in the “third-party-programs.txt” or +other similarly named text file. + +Third party programs and their corresponding required notices and/or license +terms are listed below. + +-------------------------------------------------------------------------------- + +1. Nothings STB Libraries + +stb/LICENSE + + This software is available under 2 licenses -- choose whichever you prefer. + ------------------------------------------------------------------------------ + ALTERNATIVE A - MIT License + Copyright (c) 2017 Sean Barrett + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is furnished to do + so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + ------------------------------------------------------------------------------ + ALTERNATIVE B - Public Domain (www.unlicense.org) + This is free and unencumbered software released into the public domain. + Anyone is free to copy, modify, publish, use, compile, sell, or distribute this + software, either in source code form or as a compiled binary, for any purpose, + commercial or non-commercial, and by any means. + In jurisdictions that recognize copyright laws, the author or authors of this + software dedicate any and all copyright interest in the software to the public + domain. We make this dedication for the benefit of the public at large and to + the detriment of our heirs and successors. We intend this dedication to be an + overt act of relinquishment in perpetuity of all present and future rights to + this software under copyright law. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +-------------------------------------------------------------------------------- + +2. FGPA example designs-gzip + + SDL2.0 + +zlib License + + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + +-------------------------------------------------------------------------------- + +3. Nbody + (c) 2019 Fabio Baruffa + + Plotly.js + Copyright (c) 2020 Plotly, Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +© 2020 GitHub, Inc. + +-------------------------------------------------------------------------------- + +4. GNU-EFI + Copyright (c) 1998-2000 Intel Corporation + +The files in the "lib" and "inc" subdirectories are using the EFI Application +Toolkit distributed by Intel at http://developer.intel.com/technology/efi + +This code is covered by the following agreement: + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INTEL BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. THE EFI SPECIFICATION AND ALL OTHER INFORMATION +ON THIS WEB SITE ARE PROVIDED "AS IS" WITH NO WARRANTIES, AND ARE SUBJECT +TO CHANGE WITHOUT NOTICE. + +-------------------------------------------------------------------------------- + +5. Edk2 + Copyright (c) 2019, Intel Corporation. All rights reserved. + + Edk2 Basetools + Copyright (c) 2019, Intel Corporation. All rights reserved. + +SPDX-License-Identifier: BSD-2-Clause-Patent + +-------------------------------------------------------------------------------- + +6. Heat Transmission + +GNU LESSER GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. + +0. Additional Definitions. +As used herein, “this License” refers to version 3 of the GNU Lesser General Public License, and the “GNU GPL” refers to version 3 of the GNU General Public License. + +“The Library” refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. + +An “Application” is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. + +A “Combined Work” is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the “Linked Version”. + +The “Minimal Corresponding Source” for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. + +The “Corresponding Application Code” for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. + +1. Exception to Section 3 of the GNU GPL. +You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. + +2. Conveying Modified Versions. +If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: + +a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or +b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. +3. Object Code Incorporating Material from Library Header Files. +The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: + +a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. +b) Accompany the object code with a copy of the GNU GPL and this license document. +4. Combined Works. +You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: + +a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. +b) Accompany the Combined Work with a copy of the GNU GPL and this license document. +c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. +d) Do one of the following: +0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. +1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. +e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) +5. Combined Libraries. +You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: + +a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. +b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. +6. Revised Versions of the GNU Lesser General Public License. +The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. + +If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. + +-------------------------------------------------------------------------------- +7. Rodinia + Copyright (c)2008-2011 University of Virginia +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted without royalty fees or other restrictions, provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of the University of Virginia, the Dept. of Computer Science, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF VIRGINIA OR THE SOFTWARE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +If you use this software or a modified version of it, please cite the most relevant among the following papers: + + - M. A. Goodrum, M. J. Trotter, A. Aksel, S. T. Acton, and K. Skadron. Parallelization of Particle Filter Algorithms. In Proceedings of the 3rd Workshop on Emerging Applications and Many-core Architecture (EAMA), in conjunction with the IEEE/ACM International +Symposium on Computer Architecture (ISCA), June 2010. + + - S. Che, M. Boyer, J. Meng, D. Tarjan, J. W. Sheaffer, Sang-Ha Lee and K. Skadron. +Rodinia: A Benchmark Suite for Heterogeneous Computing. IEEE International Symposium +on Workload Characterization, Oct 2009. + +- J. Meng and K. Skadron. "Performance Modeling and Automatic Ghost Zone Optimization +for Iterative Stencil Loops on GPUs." In Proceedings of the 23rd Annual ACM International +Conference on Supercomputing (ICS), June 2009. + +- L.G. Szafaryn, K. Skadron and J. Saucerman. "Experiences Accelerating MATLAB Systems +Biology Applications." in Workshop on Biomedicine in Computing (BiC) at the International +Symposium on Computer Architecture (ISCA), June 2009. + +- M. Boyer, D. Tarjan, S. T. Acton, and K. Skadron. "Accelerating Leukocyte Tracking using CUDA: +A Case Study in Leveraging Manycore Coprocessors." In Proceedings of the International Parallel +and Distributed Processing Symposium (IPDPS), May 2009. + +- S. Che, M. Boyer, J. Meng, D. Tarjan, J. W. Sheaffer, and K. Skadron. "A Performance +Study of General Purpose Applications on Graphics Processors using CUDA" Journal of +Parallel and Distributed Computing, Elsevier, June 2008. + +-------------------------------------------------------------------------------- +Other names and brands may be claimed as the property of others. + +-------------------------------------------------------------------------------- \ No newline at end of file diff --git a/AI-and-Analytics/Getting-Started-Samples/README.md b/AI-and-Analytics/Getting-Started-Samples/README.md index 4aa716713c..14154dc9fd 100644 --- a/AI-and-Analytics/Getting-Started-Samples/README.md +++ b/AI-and-Analytics/Getting-Started-Samples/README.md @@ -27,5 +27,6 @@ Third party program Licenses can be found here: [third-party-programs.txt](https |Classical Machine Learning| Scikit-learn (OneDAL) | [Intel_Extension_For_SKLearn_GettingStarted](Intel_Extension_For_SKLearn_GettingStarted) | Speed up a scikit-learn application using Intel oneDAL. |Deep Learning
Inference Optimization|Intel® Extension of TensorFlow | [Intel® Extension For TensorFlow GettingStarted](Intel_Extension_For_TensorFlow_GettingStarted) | Guides users how to run a TensorFlow inference workload on both GPU and CPU. |Deep Learning Inference Optimization|oneCCL Bindings for PyTorch | [Intel oneCCL Bindings For PyTorch GettingStarted](Intel_oneCCL_Bindings_For_PyTorch_GettingStarted) | Guides users through the process of running a simple PyTorch* distributed workload on both GPU and CPU. | +|Inference Optimization|JAX Getting Started Sample | [IntelJAX GettingStarted](https://github.com/oneapi-src/oneAPI-samples/tree/development/AI-and-Analytics/Getting-Started-Samples/IntelJAX_GettingStarted) | The JAX Getting Started sample demonstrates how to train a JAX model and run inference on Intel® hardware. | *Other names and brands may be claimed as the property of others. [Trademarks](https://www.intel.com/content/www/us/en/legal/trademarks.html) From 52a3a7634d40398134e2658ce000b0973f2da6b7 Mon Sep 17 00:00:00 2001 From: Urszula Guminska Date: Tue, 17 Dec 2024 15:26:17 +0100 Subject: [PATCH 4/7] Migrate AI code samples from numba-dpex to dpnp for 2025.0 AI Tools release --- ...elPython_GPU_dpnp_Genetic_Algorithm.ipynb} | 104 ++++++------- ...IntelPython_GPU_dpnp_Genetic_Algorithm.py} | 105 ++++++------- .../License.txt | 0 .../README.md | 18 +-- .../assets/crossover.png | Bin .../assets/mutation.png | Bin .../assets/selection.png | Bin .../requirements.txt | 0 .../sample.json | 8 +- .../third-party-programs.txt | 0 .../IntelPython_Numpy_Numba_dpnp_kNN.ipynb} | 139 ++++-------------- .../IntelPython_Numpy_Numba_dpnp_kNN.py} | 0 .../License.txt | 0 .../README.md | 12 +- .../sample.json | 10 +- .../third-party-programs.txt | 0 16 files changed, 159 insertions(+), 237 deletions(-) rename AI-and-Analytics/Features-and-Functionality/{IntelPython_GPU_numba-dpex_Genetic_Algorithm/IntelPython_GPU_numba-dpex_Genetic_Algorithm.ipynb => IntelPython_GPU_dpnp_Genetic_Algorithm/IntelPython_GPU_dpnp_Genetic_Algorithm.ipynb} (88%) rename AI-and-Analytics/Features-and-Functionality/{IntelPython_GPU_numba-dpex_Genetic_Algorithm/IntelPython_GPU_numba-dpex_Genetic_Algorithm.py => IntelPython_GPU_dpnp_Genetic_Algorithm/IntelPython_GPU_dpnp_Genetic_Algorithm.py} (86%) rename AI-and-Analytics/Features-and-Functionality/{IntelPython_GPU_numba-dpex_Genetic_Algorithm => IntelPython_GPU_dpnp_Genetic_Algorithm}/License.txt (100%) rename AI-and-Analytics/Features-and-Functionality/{IntelPython_GPU_numba-dpex_Genetic_Algorithm => IntelPython_GPU_dpnp_Genetic_Algorithm}/README.md (82%) rename AI-and-Analytics/Features-and-Functionality/{IntelPython_GPU_numba-dpex_Genetic_Algorithm => IntelPython_GPU_dpnp_Genetic_Algorithm}/assets/crossover.png (100%) rename AI-and-Analytics/Features-and-Functionality/{IntelPython_GPU_numba-dpex_Genetic_Algorithm => IntelPython_GPU_dpnp_Genetic_Algorithm}/assets/mutation.png (100%) rename AI-and-Analytics/Features-and-Functionality/{IntelPython_GPU_numba-dpex_Genetic_Algorithm => IntelPython_GPU_dpnp_Genetic_Algorithm}/assets/selection.png (100%) rename AI-and-Analytics/Features-and-Functionality/{IntelPython_GPU_numba-dpex_Genetic_Algorithm => IntelPython_GPU_dpnp_Genetic_Algorithm}/requirements.txt (100%) rename AI-and-Analytics/Features-and-Functionality/{IntelPython_GPU_numba-dpex_Genetic_Algorithm => IntelPython_GPU_dpnp_Genetic_Algorithm}/sample.json (76%) rename AI-and-Analytics/Features-and-Functionality/{IntelPython_GPU_numba-dpex_Genetic_Algorithm => IntelPython_GPU_dpnp_Genetic_Algorithm}/third-party-programs.txt (100%) rename AI-and-Analytics/Features-and-Functionality/{IntelPython_Numpy_Numba_dpex_kNN/IntelPython_Numpy_Numba_dpex_kNN.ipynb => IntelPython_Numpy_Numba_dpnp_kNN/IntelPython_Numpy_Numba_dpnp_kNN.ipynb} (71%) rename AI-and-Analytics/Features-and-Functionality/{IntelPython_Numpy_Numba_dpex_kNN/IntelPython_Numpy_Numba_dpex_kNN.py => IntelPython_Numpy_Numba_dpnp_kNN/IntelPython_Numpy_Numba_dpnp_kNN.py} (100%) rename AI-and-Analytics/Features-and-Functionality/{IntelPython_Numpy_Numba_dpex_kNN => IntelPython_Numpy_Numba_dpnp_kNN}/License.txt (100%) rename AI-and-Analytics/Features-and-Functionality/{IntelPython_Numpy_Numba_dpex_kNN => IntelPython_Numpy_Numba_dpnp_kNN}/README.md (86%) rename AI-and-Analytics/Features-and-Functionality/{IntelPython_Numpy_Numba_dpex_kNN => IntelPython_Numpy_Numba_dpnp_kNN}/sample.json (76%) rename AI-and-Analytics/Features-and-Functionality/{IntelPython_Numpy_Numba_dpex_kNN => IntelPython_Numpy_Numba_dpnp_kNN}/third-party-programs.txt (100%) diff --git a/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm/IntelPython_GPU_numba-dpex_Genetic_Algorithm.ipynb b/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm/IntelPython_GPU_dpnp_Genetic_Algorithm.ipynb similarity index 88% rename from AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm/IntelPython_GPU_numba-dpex_Genetic_Algorithm.ipynb rename to AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm/IntelPython_GPU_dpnp_Genetic_Algorithm.ipynb index adc897a9ba..016f076fee 100644 --- a/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm/IntelPython_GPU_numba-dpex_Genetic_Algorithm.ipynb +++ b/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm/IntelPython_GPU_dpnp_Genetic_Algorithm.ipynb @@ -17,9 +17,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Genetic Algorithms on GPU using Intel Distribution of Python numba-dpex\n", + "# Genetic Algorithms on GPU using Intel Distribution of Python \n", "\n", - "This code sample shows how to implement a basic genetic algorithm with Data Parallel Python using numba-dpex.\n", + "This code sample shows how to implement a basic genetic algorithm with Data Parallel Python using Data Parallel Extension of NumPy.\n", "\n", "## Genetic algorithms\n", "\n", @@ -98,7 +98,7 @@ "\n", "### Simple evaluation method\n", "\n", - "We are starting with a simple genome evaluation function. This will be our baseline and comparison for numba-dpex.\n", + "We are starting with a simple genome evaluation function. This will be our baseline and comparison for dpnp.\n", "In this example, the fitness of an individual is computed by an arbitrary set of algebraic operations on the chromosome." ] }, @@ -317,9 +317,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## GPU execution using numba-dpex\n", + "## GPU execution using dpnp\n", "\n", - "We need to start with new population initialization, as we want to perform the same operations but now on GPU using numba-dpex implementation.\n", + "We need to start with new population initialization, as we want to perform the same operations but now on GPU using dpnpx implementation.\n", "\n", "We are setting random seed the same as before to reproduce the results. " ] @@ -344,11 +344,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Evaluation function using numba-dpex\n", + "### Evaluation function using Data Parallel Extension for NumPy\n", "\n", - "The only par that differs form the standard implementation is the evaluation function.\n", + "The only part that differs form the standard implementation is the evaluation function.\n", "\n", - "The most important part is to specify the index of the computation. This is the current index of the computed chromosomes. This serves as a loop function across all chromosomes." + "In this implementation we are taking benefit from vectorized operations. DPNP will automatically vectorize addition, substraction, multiplication operations, making them efficient and suitable for GPU acceleration." ] }, { @@ -364,31 +364,28 @@ }, "outputs": [], "source": [ - "import numba_dpex\n", - "from numba_dpex import kernel_api\n", + "import dpnp as dpnp\n", "\n", - "@numba_dpex.kernel\n", - "def eval_genomes_sycl_kernel(item: kernel_api.Item, chromosomes, fitnesses, chrom_length):\n", - " pos = item.get_id(0)\n", + "def eval_genomes_dpnp(chromosomes_list, fitnesses):\n", " num_loops = 3000\n", - " for i in range(num_loops):\n", - " fitnesses[pos] += chromosomes[pos*chrom_length + 1]\n", - " for i in range(num_loops):\n", - " fitnesses[pos] -= chromosomes[pos*chrom_length + 2]\n", - " for i in range(num_loops):\n", - " fitnesses[pos] += chromosomes[pos*chrom_length + 3]\n", "\n", - " if (fitnesses[pos] < 0):\n", - " fitnesses[pos] = 0" + " # Calculate fitnesses using vectorized operations\n", + " fitnesses += chromosomes_list[:, 1] * num_loops\n", + " fitnesses -= chromosomes_list[:, 2] * num_loops\n", + " fitnesses += chromosomes_list[:, 3] * num_loops\n", + "\n", + " # Clip negative fitness values to zero\n", + " fitnesses = np.where(fitnesses < 0, 0, fitnesses)\n", + " return fitnesses" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Now, we can measure the time to perform some generations of the Genetic Algorithm with Data Parallel Python Numba dpex. \n", + "Now, we can measure the time to perform some generations of the Genetic Algorithm with Data Parallel Python Extension for NumPy. \n", "\n", - "Similarly like before, the time of the evaluation, creation of new generation and fitness wipe are measured for GPU execution. But first, we need to send all the chromosomes and fitnesses container to the chosen device. " + "Similarly like before, the time of the evaluation, creation of new generation and fitness wipe are measured for GPU execution. But first, we need to send all the chromosomes and fitnesses container to the chosen device - GPU. " ] }, { @@ -399,25 +396,26 @@ }, "outputs": [], "source": [ - "import dpnp\n", - "\n", "print(\"SYCL:\")\n", "start = time.time()\n", "\n", "# Genetic Algorithm on GPU\n", "for i in range(num_generations):\n", " print(\"Gen \" + str(i+1) + \"/\" + str(num_generations))\n", - " chromosomes_flat = chromosomes.flatten()\n", - " chromosomes_flat_dpctl = dpnp.asarray(chromosomes_flat, device=\"gpu\")\n", - " fitnesses_dpctl = dpnp.asarray(fitnesses, device=\"gpu\")\n", + " chromosomes_dpctl = chromosomes\n", + " fitnesses_dpctl = fitnesses\n", + " try:\n", + " chromosomes_dpctl = dpnp.asarray(chromosomes, device=\"gpu\")\n", + " fitnesses_dpctl = dpnp.asarray(fitnesses, device=\"gpu\")\n", + " except Exception:\n", + " print(\"GPU device is not available\")\n", + " \n", + " fitnesses = eval_genomes_dpnp(chromosomes, fitnesses)\n", " \n", - " exec_range = kernel_api.Range(pop_size)\n", - " numba_dpex.call_kernel(eval_genomes_sycl_kernel, exec_range, chromosomes_flat_dpctl, fitnesses_dpctl, chrom_size)\n", " fitnesses = dpnp.asnumpy(fitnesses_dpctl)\n", " chromosomes = next_generation(chromosomes, fitnesses)\n", " fitnesses = np.zeros(pop_size, dtype=np.float32)\n", "\n", - "\n", "end = time.time()\n", "time_sycl = end-start\n", "print(\"time elapsed: \" + str((time_sycl)))\n", @@ -457,7 +455,7 @@ "\n", "plt.figure()\n", "plt.title(\"Time comparison\")\n", - "plt.bar([\"Numba_dpex\", \"without optimization\"], [time_sycl, time_cpu])\n", + "plt.bar([\"DPNP\", \"without optimization\"], [time_sycl, time_cpu])\n", "\n", "plt.show()" ] @@ -546,7 +544,7 @@ "\n", "The evaluate created generation we are calculating the full distance of the given path (chromosome). In this example, the lower the fitness value is, the better the chromosome. That's different from the general GA that we implemented.\n", "\n", - "As in this example we are also using numba-dpex, we are using an index like before." + "As in the previous example dpnp will vectorize basic mathematical operations to take benefit from optimizations." ] }, { @@ -555,11 +553,11 @@ "metadata": {}, "outputs": [], "source": [ - "@numba_dpex.kernel\n", - "def eval_genomes_plain_TSP_SYCL(item: kernel_api.Item, chromosomes, fitnesses, distances, pop_length):\n", - " pos = item.get_id(1)\n", - " for j in range(pop_length-1):\n", - " fitnesses[pos] += distances[int(chromosomes[pos, j]), int(chromosomes[pos, j+1])]\n" + "def eval_genomes_plain_TSP_SYCL(chromosomes, fitnesses, distances, pop_length):\n", + " for pos in range(pop_length):\n", + " for j in range(chromosomes.shape[1]-1):\n", + " fitnesses[pos] += distances[int(chromosomes[pos, j]), int(chromosomes[pos, j+1])]\n", + " return fitnesses\n" ] }, { @@ -703,22 +701,26 @@ "source": [ "print(\"Traveling Salesman Problem:\")\n", "\n", - "distances_dpctl = dpnp.asarray(distances, device=\"gpu\")\n", + "distances_dpnp = distances\n", + "try:\n", + " distances_dpnp = dpnp.asarray(distances, device=\"gpu\")\n", + "except Exception:\n", + " print(\"GPU device is not available\")\n", + "\n", "# Genetic Algorithm on GPU\n", "for i in range(num_generations):\n", " print(\"Gen \" + str(i+1) + \"/\" + str(num_generations))\n", - " chromosomes_flat_dpctl = dpnp.asarray(chromosomes, device=\"gpu\")\n", - " fitnesses_dpctl = dpnp.asarray(fitnesses.copy(), device=\"gpu\")\n", "\n", - " exec_range = kernel_api.Range(pop_size)\n", - " numba_dpex.call_kernel(eval_genomes_plain_TSP_SYCL, exec_range, chromosomes_flat_dpctl, fitnesses_dpctl, distances_dpctl, pop_size)\n", - " fitnesses = dpnp.asnumpy(fitnesses_dpctl)\n", - " chromosomes = next_generation_TSP(chromosomes, fitnesses)\n", + " chromosomes_dpnp = chromosomes\n", + " try:\n", + " chromosomes_dpnp = dpnp.asarray(chromosomes, device=\"gpu\")\n", + " except Exception:\n", + " print(\"GPU device is not available\")\n", + "\n", " fitnesses = np.zeros(pop_size, dtype=np.float32)\n", "\n", - "for i in range(len(chromosomes)):\n", - " for j in range(11):\n", - " fitnesses[i] += distances[int(chromosomes[i][j])][int(chromosomes[i][j+1])]\n", + " fitnesses = eval_genomes_plain_TSP_SYCL(chromosomes_dpnp, fitnesses, distances_dpnp, pop_size)\n", + " chromosomes = next_generation_TSP(chromosomes, fitnesses)\n", "\n", "fitness_pairs = []\n", "\n", @@ -736,7 +738,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "In this code sample, there was a general purpose Genetic Algorithm created and optimized using numba-dpex to run on GPU. Then the same approach was applied to the Traveling Salesman Problem." + "In this code sample, there was a general purpose Genetic Algorithm created and optimized using dpnp to run on GPU. Then the same approach was applied to the Traveling Salesman Problem." ] }, { @@ -756,7 +758,7 @@ "provenance": [] }, "kernelspec": { - "display_name": "base", + "display_name": "Base", "language": "python", "name": "base" }, @@ -770,7 +772,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.18" + "version": "3.9.19" } }, "nbformat": 4, diff --git a/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm/IntelPython_GPU_numba-dpex_Genetic_Algorithm.py b/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm/IntelPython_GPU_dpnp_Genetic_Algorithm.py similarity index 86% rename from AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm/IntelPython_GPU_numba-dpex_Genetic_Algorithm.py rename to AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm/IntelPython_GPU_dpnp_Genetic_Algorithm.py index c33bb2e1c1..6cde827960 100644 --- a/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm/IntelPython_GPU_numba-dpex_Genetic_Algorithm.py +++ b/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm/IntelPython_GPU_dpnp_Genetic_Algorithm.py @@ -11,9 +11,9 @@ # ============================================================= -# # Genetic Algorithms on GPU using Intel Distribution of Python numba-dpex +# # Genetic Algorithms on GPU using Intel Distribution of Python # -# This code sample shows how to implement a basic genetic algorithm with Data Parallel Python using numba-dpex. +# This code sample shows how to implement a basic genetic algorithm with Data Parallel Python using Data Parallel Extension of NumPy. # # ## Genetic algorithms # @@ -67,7 +67,7 @@ # # ### Simple evaluation method # -# We are starting with a simple genome evaluation function. This will be our baseline and comparison for numba-dpex. +# We are starting with a simple genome evaluation function. This will be our baseline and comparison for dpnp. # In this example, the fitness of an individual is computed by an arbitrary set of algebraic operations on the chromosome. # In[ ]: @@ -150,7 +150,6 @@ def mutation(child_sequence, chance=0.01): # It allows fitness proportional selection - the bigger the fitness value, the bigger the chance that a given chromosome will be selected. # # The result of all the operations is returned as chromosomes. -# # In[ ]: @@ -239,9 +238,9 @@ def next_generation(chromosomes, fitnesses): print("First chromosome: " + str(chromosomes[0])) -# ## GPU execution using numba-dpex +# ## GPU execution using dpnp # -# We need to start with new population initialization, as we want to perform the same operations but now on GPU using numba-dpex implementation. +# We need to start with new population initialization, as we want to perform the same operations but now on GPU using dpnpx implementation. # # We are setting random seed the same as before to reproduce the results. @@ -256,58 +255,57 @@ def next_generation(chromosomes, fitnesses): chromosomes[i][j] = random.uniform(0,1) -# ### Evaluation function using numba-dpex +# ### Evaluation function using Data Parallel Extension for NumPy # -# The only par that differs form the standard implementation is the evaluation function. +# The only part that differs form the standard implementation is the evaluation function. # -# The most important part is to specify the index of the computation. This is the current index of the computed chromosomes. This serves as a loop function across all chromosomes. +# In this implementation we are taking benefit from vectorized operations. DPNP will automatically vectorize addition, substraction, multiplication operations, making them efficient and suitable for GPU acceleration. # In[ ]: -import numba_dpex -from numba_dpex import kernel_api +import dpnp as dpnp -@numba_dpex.kernel -def eval_genomes_sycl_kernel(item: kernel_api.Item, chromosomes, fitnesses, chrom_length): - pos = item.get_id(0) +def eval_genomes_dpnp(chromosomes_list, fitnesses): num_loops = 3000 - for i in range(num_loops): - fitnesses[pos] += chromosomes[pos*chrom_length + 1] - for i in range(num_loops): - fitnesses[pos] -= chromosomes[pos*chrom_length + 2] - for i in range(num_loops): - fitnesses[pos] += chromosomes[pos*chrom_length + 3] - if (fitnesses[pos] < 0): - fitnesses[pos] = 0 + # Calculate fitnesses using vectorized operations + fitnesses += chromosomes_list[:, 1] * num_loops + fitnesses -= chromosomes_list[:, 2] * num_loops + fitnesses += chromosomes_list[:, 3] * num_loops + + # Clip negative fitness values to zero + fitnesses = np.where(fitnesses < 0, 0, fitnesses) + return fitnesses -# Now, we can measure the time to perform some generations of the Genetic Algorithm with Data Parallel Python Numba dpex. + +# Now, we can measure the time to perform some generations of the Genetic Algorithm with Data Parallel Python Extension for NumPy. # -# Similarly like before, the time of the evaluation, creation of new generation and fitness wipe are measured for GPU execution. But first, we need to send all the chromosomes and fitnesses container to the chosen device. +# Similarly like before, the time of the evaluation, creation of new generation and fitness wipe are measured for GPU execution. But first, we need to send all the chromosomes and fitnesses container to the chosen device - GPU. # In[ ]: -import dpnp - print("SYCL:") start = time.time() # Genetic Algorithm on GPU for i in range(num_generations): print("Gen " + str(i+1) + "/" + str(num_generations)) - chromosomes_flat = chromosomes.flatten() - chromosomes_flat_dpctl = dpnp.asarray(chromosomes_flat, device="gpu") - fitnesses_dpctl = dpnp.asarray(fitnesses, device="gpu") - - exec_range = kernel_api.Range(pop_size) - numba_dpex.call_kernel(eval_genomes_sycl_kernel, exec_range, chromosomes_flat_dpctl, fitnesses_dpctl, chrom_size) + chromosomes_dpctl = chromosomes + fitnesses_dpctl = fitnesses + try: + chromosomes_dpctl = dpnp.asarray(chromosomes, device="gpu") + fitnesses_dpctl = dpnp.asarray(fitnesses, device="gpu") + except Exception: + print("GPU device is not available") + + fitnesses = eval_genomes_dpnp(chromosomes, fitnesses) + fitnesses = dpnp.asnumpy(fitnesses_dpctl) chromosomes = next_generation(chromosomes, fitnesses) fitnesses = np.zeros(pop_size, dtype=np.float32) - end = time.time() time_sycl = end-start print("time elapsed: " + str((time_sycl))) @@ -331,7 +329,7 @@ def eval_genomes_sycl_kernel(item: kernel_api.Item, chromosomes, fitnesses, chro plt.figure() plt.title("Time comparison") -plt.bar(["Numba_dpex", "without optimization"], [time_sycl, time_cpu]) +plt.bar(["DPNP", "without optimization"], [time_sycl, time_cpu]) plt.show() @@ -400,16 +398,16 @@ def eval_genomes_sycl_kernel(item: kernel_api.Item, chromosomes, fitnesses, chro # # The evaluate created generation we are calculating the full distance of the given path (chromosome). In this example, the lower the fitness value is, the better the chromosome. That's different from the general GA that we implemented. # -# As in this example we are also using numba-dpex, we are using an index like before. +# As in the previous example dpnp will vectorize basic mathematical operations to take benefit from optimizations. # In[ ]: -@numba_dpex.kernel -def eval_genomes_plain_TSP_SYCL(item: kernel_api.Item, chromosomes, fitnesses, distances, pop_length): - pos = item.get_id(1) - for j in range(pop_length-1): - fitnesses[pos] += distances[int(chromosomes[pos, j]), int(chromosomes[pos, j+1])] +def eval_genomes_plain_TSP_SYCL(chromosomes, fitnesses, distances, pop_length): + for pos in range(pop_length): + for j in range(chromosomes.shape[1]-1): + fitnesses[pos] += distances[int(chromosomes[pos, j]), int(chromosomes[pos, j+1])] + return fitnesses # ### Crossover @@ -521,22 +519,26 @@ def next_generation_TSP(chromosomes, fitnesses): print("Traveling Salesman Problem:") -distances_dpctl = dpnp.asarray(distances, device="gpu") +distances_dpnp = distances +try: + distances_dpnp = dpnp.asarray(distances, device="gpu") +except Exception: + print("GPU device is not available") + # Genetic Algorithm on GPU for i in range(num_generations): print("Gen " + str(i+1) + "/" + str(num_generations)) - chromosomes_flat_dpctl = dpnp.asarray(chromosomes, device="gpu") - fitnesses_dpctl = dpnp.asarray(fitnesses.copy(), device="gpu") - exec_range = kernel_api.Range(pop_size) - numba_dpex.call_kernel(eval_genomes_plain_TSP_SYCL, exec_range, chromosomes_flat_dpctl, fitnesses_dpctl, distances_dpctl, pop_size) - fitnesses = dpnp.asnumpy(fitnesses_dpctl) - chromosomes = next_generation_TSP(chromosomes, fitnesses) + chromosomes_dpnp = chromosomes + try: + chromosomes_dpnp = dpnp.asarray(chromosomes, device="gpu") + except Exception: + print("GPU device is not available") + fitnesses = np.zeros(pop_size, dtype=np.float32) -for i in range(len(chromosomes)): - for j in range(11): - fitnesses[i] += distances[int(chromosomes[i][j])][int(chromosomes[i][j+1])] + fitnesses = eval_genomes_plain_TSP_SYCL(chromosomes_dpnp, fitnesses, distances_dpnp, pop_size) + chromosomes = next_generation_TSP(chromosomes, fitnesses) fitness_pairs = [] @@ -550,9 +552,10 @@ def next_generation_TSP(chromosomes, fitnesses): print("Worst path: ", sorted_pairs[-1][0], " distance: ", sorted_pairs[-1][1]) -# In this code sample, there was a general purpose Genetic Algorithm created and optimized using numba-dpex to run on GPU. Then the same approach was applied to the Traveling Salesman Problem. +# In this code sample, there was a general purpose Genetic Algorithm created and optimized using dpnp to run on GPU. Then the same approach was applied to the Traveling Salesman Problem. # In[ ]: print("[CODE_SAMPLE_COMPLETED_SUCCESFULLY]") + diff --git a/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm/License.txt b/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm/License.txt similarity index 100% rename from AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm/License.txt rename to AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm/License.txt diff --git a/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm/README.md b/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm/README.md similarity index 82% rename from AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm/README.md rename to AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm/README.md index 3c7c97d4ee..55fb54864e 100644 --- a/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm/README.md +++ b/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm/README.md @@ -1,18 +1,18 @@ -# `Genetic Algorithms on GPU using Intel® Distribution for Python* numba-dpex` Sample +# `Genetic Algorithms on GPU using Intel® Distribution for Python* dpnp` Sample -The `Genetic Algorithms on GPU using Intel® Distribution for Python* numba-dpex` sample shows how to implement a general genetic algorithm (GA) and offload computation to a GPU using numba-dpex. +The `Genetic Algorithms on GPU using Intel® Distribution for Python* dpnp` sample shows how to implement a general genetic algorithm (GA) and offload computation to a GPU using dpnp. | Property | Description | :--- | :--- | Category | Code Optimization -| What you will learn | How to implement the genetic algorithm using the Data-parallel Extension for Numba* (numba-dpex)? +| What you will learn | How to implement the genetic algorithm using the Data-parallel Extension for NumPy* (dpnp)? | Time to complete | 8 minutes >**Note**: This sample is validated on Intel® Distribution for Python* Offline Installer and AI Tools Offline Installer. For the full list of validated platforms refer to [Platform Validation](https://github.com/oneapi-src/oneAPI-samples/tree/master?tab=readme-ov-file#platform-validation). ## Purpose -In this sample, you will create and run the general genetic algorithm and optimize it to run on GPU using the Intel® Distribution for Python* numba-dpex. You will learn what are selection, crossover, and mutation, and how to adjust those methods from general genetic algorithm to a specific optimization problem which is the Traveling Salesman Problem. +In this sample, you will create and run the general genetic algorithm and optimize it to run on GPU using the Intel® Distribution for Python* dpnp. You will learn what are selection, crossover, and mutation, and how to adjust those methods from general genetic algorithm to a specific optimization problem which is the Traveling Salesman Problem. ## Prerequisites @@ -24,7 +24,7 @@ In this sample, you will create and run the general genetic algorithm and optimi ## Key Implementation Details -This sample code is implemented for GPUs using Python. The sample assumes you have numba-dpex installed inside a Conda environment, similar to what is installed with the Intel® Distribution for Python*. +This sample code is implemented for GPUs using Python. The sample assumes you have dpnp installed inside a Conda environment, similar to what is installed with the Intel® Distribution for Python*. The sample tutorial contains one Jupyter Notebook and one Python script. You can use either. @@ -53,7 +53,7 @@ cd oneAPI-samples/AI-and-Analytics// - ``` git clone https://github.com/oneapi-src/oneAPI-samples.git -cd oneAPI-samples/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm +cd oneAPI-samples/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm ``` **4. Install dependencies** @@ -67,7 +67,7 @@ pip install notebook For Jupyter Notebook, refer to [Installing Jupyter](https://jupyter.org/install) for detailed installation instructions. ## Run the Sample ->**Note**: Before running the sample, make sure [Environment Setup](https://github.com/oneapi-src/oneAPI-samples/tree/master/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm#environment-setup) is completed. +>**Note**: Before running the sample, make sure [Environment Setup](https://github.com/oneapi-src/oneAPI-samples/tree/master/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm#environment-setup) is completed. ### Intel® Distribution for Python* Offline Installer (Validated) @@ -91,7 +91,7 @@ jupyter notebook --ip=0.0.0.0 **4. Select the Notebook** ``` -IntelPython_GPU_numba-dpex_Genetic_Algorithm.ipynb +IntelPython_GPU_dpnp_Genetic_Algorithm.ipynb ``` **5. Change the kernel to `base`** @@ -99,7 +99,7 @@ IntelPython_GPU_numba-dpex_Genetic_Algorithm.ipynb ## Example Output -If successful, the sample displays `[CODE_SAMPLE_COMPLETED_SUCCESSFULLY]` at the end of execution. The sample will print out the runtimes and charts of relative performance with numba-dpex and without any optimizations as the baseline. Additionally, sample will print the best and worst path found in the Traveling Salesman Problem. +If successful, the sample displays `[CODE_SAMPLE_COMPLETED_SUCCESSFULLY]` at the end of execution. The sample will print out the runtimes and charts of relative performance with dpnp and without any optimizations as the baseline. Additionally, sample will print the best and worst path found in the Traveling Salesman Problem. ## Related Samples diff --git a/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm/assets/crossover.png b/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm/assets/crossover.png similarity index 100% rename from AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm/assets/crossover.png rename to AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm/assets/crossover.png diff --git a/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm/assets/mutation.png b/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm/assets/mutation.png similarity index 100% rename from AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm/assets/mutation.png rename to AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm/assets/mutation.png diff --git a/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm/assets/selection.png b/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm/assets/selection.png similarity index 100% rename from AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm/assets/selection.png rename to AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm/assets/selection.png diff --git a/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm/requirements.txt b/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm/requirements.txt similarity index 100% rename from AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm/requirements.txt rename to AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm/requirements.txt diff --git a/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm/sample.json b/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm/sample.json similarity index 76% rename from AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm/sample.json rename to AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm/sample.json index 4ad8027821..4d2dead081 100644 --- a/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm/sample.json +++ b/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm/sample.json @@ -1,8 +1,8 @@ { "guid": "DB33884A-2DDE-4657-9F95-E6E573403F61", - "name": "Genetic Algorithms on GPU using Intel® Distribution of Python numba-dpex", + "name": "Genetic Algorithms on GPU using Intel® Distribution of Python dpnp", "categories": ["Toolkit/oneAPI AI And Analytics/Getting Started"], - "description": "This sample shows how to implement general genetic algorithm (GA) and offload computation to GPU using numba-dpex.", + "description": "This sample shows how to implement general genetic algorithm (GA) and offload computation to GPU using dpnp.", "builder": ["cli"], "languages": [{"python":{}}], "dependencies": ["intelpython"], @@ -16,9 +16,9 @@ "conda activate base", "pip install -r requirements.txt" ], - "id": "idp_ga_numba_dpex_py", + "id": "idp_ga_dpnp_py", "steps": [ - "python IntelPython_GPU_numba-dpex_Genetic_Algorithm.py" + "python IntelPython_GPU_dpnp_Genetic_Algorithm.py" ] } ] diff --git a/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm/third-party-programs.txt b/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm/third-party-programs.txt similarity index 100% rename from AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm/third-party-programs.txt rename to AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm/third-party-programs.txt diff --git a/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpex_kNN/IntelPython_Numpy_Numba_dpex_kNN.ipynb b/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpnp_kNN/IntelPython_Numpy_Numba_dpnp_kNN.ipynb similarity index 71% rename from AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpex_kNN/IntelPython_Numpy_Numba_dpex_kNN.ipynb rename to AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpnp_kNN/IntelPython_Numpy_Numba_dpnp_kNN.ipynb index 47a10db05b..37fb4c58c8 100644 --- a/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpex_kNN/IntelPython_Numpy_Numba_dpex_kNN.ipynb +++ b/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpnp_kNN/IntelPython_Numpy_Numba_dpnp_kNN.ipynb @@ -17,7 +17,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Simple k-NN classification with numba_dpex IDP optimization\n", + "# Simple k-NN classification with Data Parallel Extension for NumPy IDP optimization\n", "\n", "This sample shows how to receive the same accuracy of the k-NN model classification by using numpy, numba and numba_dpex. The computation are performed using wine dataset.\n", "\n", @@ -276,7 +276,7 @@ " counter = {}\n", " for item in neighbor_classes:\n", " if item in counter:\n", - " counter[item] = counter.get(item) + 1\n", + " counter[item] += 1\n", " else:\n", " counter[item] = 1\n", " counter_sorted = sorted(counter)\n", @@ -312,13 +312,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Numba_dpex k-NN\n", + "## Data Parallel Extension for NumPy k-NN\n", "\n", - "Numba_dpex implementation use `numba_dpex.kernel()` decorator. For more information about programming, SYCL kernels go to: https://intelpython.github.io/numba-dpex/latest/user_guides/kernel_programming_guide/index.html.\n", + "To take benefit of DPNP, we can leverage its vectorized operations and efficient algorithms to implement a k-NN algorithm. We will use optimized operations like `sum`, `sqrt` or `argsort`.\n", "\n", - "Calculating distance is like in the NumPy example. We are using Euclidean distance. Later, we create the queue of the neighbors by the calculated distance and count in provided *k* votes for dedicated classes of neighbors.\n", - "\n", - "In the end, we are taking a class that achieves the maximum value of votes and setting it for the current global iteration." + "Calculating distance is like in the NumPy example. We are using Euclidean distance. The next step is to find the indexes of k-nearest neighbours for each test poin, and get tehir labels. At the end, we neet to determine the most frequent label among k-nearest." ] }, { @@ -327,87 +325,23 @@ "metadata": {}, "outputs": [], "source": [ - "import numba_dpex\n", - "\n", - "@numba_dpex.kernel\n", - "def knn_numba_dpex(\n", - " item: numba_dpex.kernel_api.Item,\n", - " train,\n", - " train_labels,\n", - " test,\n", - " k,\n", - " predictions,\n", - " votes_to_classes_lst,\n", - "):\n", - " dtype = train.dtype\n", - " i = item.get_id(0)\n", - " queue_neighbors = numba_dpex.kernel_api.PrivateArray(shape=(3, 2), dtype=dtype)\n", - "\n", - " for j in range(k):\n", - " x1 = train[j, 0]\n", - " x2 = test[i, 0]\n", - "\n", - " distance = dtype.type(0.0)\n", - " diff = x1 - x2\n", - " distance += diff * diff\n", - " dist = math.sqrt(distance)\n", - "\n", - " queue_neighbors[j, 0] = dist\n", - " queue_neighbors[j, 1] = train_labels[j]\n", - "\n", - " for j in range(k):\n", - " new_distance = queue_neighbors[j, 0]\n", - " new_neighbor_label = queue_neighbors[j, 1]\n", - " index = j\n", - "\n", - " while index > 0 and new_distance < queue_neighbors[index - 1, 0]:\n", - " queue_neighbors[index, 0] = queue_neighbors[index - 1, 0]\n", - " queue_neighbors[index, 1] = queue_neighbors[index - 1, 1]\n", - "\n", - " index = index - 1\n", - "\n", - " queue_neighbors[index, 0] = new_distance\n", - " queue_neighbors[index, 1] = new_neighbor_label\n", - "\n", - " for j in range(k, len(train)):\n", - " x1 = train[j, 0]\n", - " x2 = test[i, 0]\n", - "\n", - " distance = dtype.type(0.0)\n", - " diff = x1 - x2\n", - " distance += diff * diff\n", - " dist = math.sqrt(distance)\n", - "\n", - " if dist < queue_neighbors[k - 1, 0]:\n", - " queue_neighbors[k - 1, 0] = dist\n", - " queue_neighbors[k - 1, 1] = train_labels[j]\n", - " new_distance = queue_neighbors[k - 1, 0]\n", - " new_neighbor_label = queue_neighbors[k - 1, 1]\n", - " index = k - 1\n", - "\n", - " while index > 0 and new_distance < queue_neighbors[index - 1, 0]:\n", - " queue_neighbors[index, 0] = queue_neighbors[index - 1, 0]\n", - " queue_neighbors[index, 1] = queue_neighbors[index - 1, 1]\n", - "\n", - " index = index - 1\n", - "\n", - " queue_neighbors[index, 0] = new_distance\n", - " queue_neighbors[index, 1] = new_neighbor_label\n", - "\n", - " votes_to_classes = votes_to_classes_lst[i]\n", - "\n", - " for j in range(len(queue_neighbors)):\n", - " votes_to_classes[int(queue_neighbors[j, 1])] += 1\n", - "\n", - " max_ind = 0\n", - " max_value = dtype.type(0)\n", - "\n", - " for j in range(3):\n", - " if votes_to_classes[j] > max_value:\n", - " max_value = votes_to_classes[j]\n", - " max_ind = j\n", - "\n", - " predictions[i] = max_ind" + "import dpnp as dpnp\n", + "\n", + "def knn_dpnp(train, train_labels, test, k):\n", + " # 1. Calculate pairwise distances between test and train points\n", + " distances = dpnp.sqrt(dpnp.sum((test[:, None, :] - train[None, :, :])**2, axis=-1))\n", + "\n", + " # 2. Find the indices of the k nearest neighbors for each test point\n", + " nearest_neighbors = dpnp.argsort(distances, axis=1)[:, :k]\n", + "\n", + " # 3. Get the labels of the nearest neighbors\n", + " nearest_labels = train_labels[nearest_neighbors]\n", + "\n", + " # 4. Determine the most frequent label among the k nearest neighbors\n", + " unique_labels, counts = np.unique(nearest_labels, return_counts=True)\n", + " predicted_labels = nearest_labels[np.argmax(counts)]\n", + "\n", + " return predicted_labels" ] }, { @@ -416,9 +350,7 @@ "source": [ "Next, like before, let's test the prepared k-NN function.\n", "\n", - "In this case, we will need to provide the container for predictions: `predictions` and the container for votes per class: `votes_to_classes_lst` (the container size is 3, as we have 3 classes in our dataset).\n", - "\n", - "We are running a prepared k-NN function on a CPU device as the input data was allocated on the CPU. Numba-dpex will infer the execution queue based on where the input arguments to the kernel were allocated. Refer: https://intelpython.github.io/oneAPI-for-SciPy/details/programming_model/#compute-follows-data" + "We are running a prepared k-NN function on a CPU device as the input data was allocated on the CPU using DPNP." ] }, { @@ -427,26 +359,11 @@ "metadata": {}, "outputs": [], "source": [ - "import dpnp\n", - "\n", - "predictions = dpnp.empty(len(X_test.values), device=\"cpu\")\n", - "# we have 3 classes\n", - "votes_to_classes_lst = dpnp.zeros((len(X_test.values), 3), device=\"cpu\")\n", - "\n", "X_train_dpt = dpnp.asarray(X_train.values, device=\"cpu\")\n", "y_train_dpt = dpnp.asarray(y_train.values, device=\"cpu\")\n", "X_test_dpt = dpnp.asarray(X_test.values, device=\"cpu\")\n", "\n", - "numba_dpex.call_kernel(\n", - " knn_numba_dpex,\n", - " numba_dpex.Range(len(X_test.values)),\n", - " X_train_dpt,\n", - " y_train_dpt,\n", - " X_test_dpt,\n", - " 3,\n", - " predictions,\n", - " votes_to_classes_lst,\n", - ")" + "pred = knn_dpnp(X_train_dpt, y_train_dpt, X_test_dpt, 3)" ] }, { @@ -465,7 +382,7 @@ "predictions_numba = dpnp.asnumpy(predictions)\n", "true_values = y_test.to_numpy()\n", "accuracy = np.mean(predictions_numba == true_values)\n", - "print(\"Numba_dpex accuracy:\", accuracy)" + "print(\"Data Parallel Extension for NumPy accuracy:\", accuracy)" ] }, { @@ -480,9 +397,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (Intel® oneAPI 2022.3)", + "display_name": "Base", "language": "python", - "name": "c009-intel_distribution_of_python_3_oneapi-beta05-python" + "name": "base" }, "language_info": { "codemirror_mode": { @@ -494,7 +411,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.13" + "version": "3.9.19" }, "vscode": { "interpreter": { diff --git a/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpex_kNN/IntelPython_Numpy_Numba_dpex_kNN.py b/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpnp_kNN/IntelPython_Numpy_Numba_dpnp_kNN.py similarity index 100% rename from AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpex_kNN/IntelPython_Numpy_Numba_dpex_kNN.py rename to AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpnp_kNN/IntelPython_Numpy_Numba_dpnp_kNN.py diff --git a/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpex_kNN/License.txt b/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpnp_kNN/License.txt similarity index 100% rename from AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpex_kNN/License.txt rename to AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpnp_kNN/License.txt diff --git a/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpex_kNN/README.md b/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpnp_kNN/README.md similarity index 86% rename from AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpex_kNN/README.md rename to AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpnp_kNN/README.md index 1e95a29ff0..dfe5b88d3d 100644 --- a/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpex_kNN/README.md +++ b/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpnp_kNN/README.md @@ -1,18 +1,18 @@ -# `Intel® Python: NumPy vs numba-dpex` Sample +# `Intel® Python: NumPy vs Numba vs DPNP` Sample -The `Intel® Python: NumPy vs numba-dpex` sample shows how to achieve the same accuracy of the k-NN model classification while using NumPy*, Numba*, and Data Parallel Extension for Numba* (numba-dpex). +The `Intel® Python: NumPy vs Numba vs DPNP` sample shows how to achieve the same accuracy of the k-NN model classification while using NumPy*, Numba*, and Data Parallel Extension for NumPy* (dpnp). | Property | Description | :--- | :--- | Category | Code Optimization -| What you will learn | How to program using the Data Parallel Extension for Numba* (numba-dpex) +| What you will learn | How to program using the Data Parallel Extension for NumPy* (dpnp) | Time to complete | 5 minutes >**Note**: This sample is validated on Intel® Distribution for Python* Offline Installer and AI Tools Offline Installer. For the full list of validated platforms refer to [Platform Validation](https://github.com/oneapi-src/oneAPI-samples/tree/master?tab=readme-ov-file#platform-validation). ## Purpose -In this sample, you will run a k-nearest neighbors algorithm using 3 different Intel® Distribution for Python* libraries: NumPy, Numba, and numba-dpex. You will learn how to use k-NN model and how to optimize it by numba-dpex operations without sacrificing accuracy. +In this sample, you will run a k-nearest neighbors algorithm using 3 different Intel® Distribution for Python* libraries: NumPy, Numba, and dpnp. You will learn how to use k-NN model and how to optimize it by dpnp operations without sacrificing accuracy. ## Prerequisites @@ -24,7 +24,7 @@ In this sample, you will run a k-nearest neighbors algorithm using 3 different I ## Key Implementation Details -This sample code is implemented for the CPU using Python. The sample assumes you have numba-dpex installed inside a Conda environment, similar to what is installed with the Intel® Distribution for Python*. +This sample code is implemented for the CPU using Python. The sample assumes you have dpnp installed inside a Conda environment, similar to what is installed with the Intel® Distribution for Python*. The sample tutorial contains one Jupyter Notebook and one Python script. You can use either. @@ -112,7 +112,7 @@ Numba_dpex accuracy 0.7222222222222222 ## Related Samples * [Get Started with the Intel® Distribution for Python*](https://www.intel.com/content/www/us/en/developer/articles/technical/get-started-with-intel-distribution-for-python.html) -* [`Genetic Algorithms on GPU using Intel® Distribution for Python* numba-dpex` Sample](https://github.com/oneapi-src/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_numba-dpex_Genetic_Algorithm/README.md) +* [`Genetic Algorithms on GPU using Intel® Distribution for Python* dpnp` Sample](https://github.com/oneapi-src/AI-and-Analytics/Features-and-Functionality/IntelPython_GPU_dpnp_Genetic_Algorithm/README.md) ## License Code samples are licensed under the MIT license. See diff --git a/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpex_kNN/sample.json b/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpnp_kNN/sample.json similarity index 76% rename from AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpex_kNN/sample.json rename to AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpnp_kNN/sample.json index 65ec27b5d8..b663ead410 100644 --- a/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpex_kNN/sample.json +++ b/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpnp_kNN/sample.json @@ -1,8 +1,8 @@ { "guid": "20ED32F3-671E-4710-A857-2821DB56CC22", - "name": "Intel® Python NumPy vs Numba_dpex", + "name": "Intel® Python NumPy vs Numba vs DPNP", "categories": ["Toolkit/oneAPI AI And Analytics/Getting Started"], - "description": "This sample shows how to achieve the same accuracy of the k-NN model classification while using numpy, numba and numba_dpex.", + "description": "This sample shows how to achieve the same accuracy of the k-NN model classification while using numpy, numba and dpnp.", "builder": ["cli"], "languages": [{"python":{}}], "dependencies": ["intelpython"], @@ -14,11 +14,11 @@ "env": [ "source /intel/oneapi/intelpython/bin/activate", "conda activate base", - "pip install pandas" + "pip install numba" ], - "id": "idp_numpy_numba_dpex_gs_py", + "id": "idp_numpy_numba_dpnp_gs_py", "steps": [ - "python IntelPython_Numpy_Numba_dpex_kNN.py" + "python IntelPython_Numpy_Numba_dpnp_kNN.py" ] } ] diff --git a/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpex_kNN/third-party-programs.txt b/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpnp_kNN/third-party-programs.txt similarity index 100% rename from AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpex_kNN/third-party-programs.txt rename to AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpnp_kNN/third-party-programs.txt From c975a6ab3f68d45617c35795b585446605a2a29a Mon Sep 17 00:00:00 2001 From: Urszula Guminska Date: Tue, 17 Dec 2024 17:20:48 +0100 Subject: [PATCH 5/7] Updated numba kNN script --- .../IntelPython_Numpy_Numba_dpnp_kNN.ipynb | 6 +- .../IntelPython_Numpy_Numba_dpnp_kNN.py | 170 +++++------------- 2 files changed, 47 insertions(+), 129 deletions(-) diff --git a/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpnp_kNN/IntelPython_Numpy_Numba_dpnp_kNN.ipynb b/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpnp_kNN/IntelPython_Numpy_Numba_dpnp_kNN.ipynb index 37fb4c58c8..8bb4efcac0 100644 --- a/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpnp_kNN/IntelPython_Numpy_Numba_dpnp_kNN.ipynb +++ b/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpnp_kNN/IntelPython_Numpy_Numba_dpnp_kNN.ipynb @@ -19,7 +19,7 @@ "source": [ "# Simple k-NN classification with Data Parallel Extension for NumPy IDP optimization\n", "\n", - "This sample shows how to receive the same accuracy of the k-NN model classification by using numpy, numba and numba_dpex. The computation are performed using wine dataset.\n", + "This sample shows how to receive the same accuracy of the k-NN model classification by using numpy, numba and dpnp. The computation are performed using wine dataset.\n", "\n", "Let's start with general imports used in the whole sample." ] @@ -73,7 +73,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We are planning to compare the results of the numpy, namba and IDP numba_dpex so we need to make sure that the results are reproducible. We can do this through the use of a random seed function that initializes a random number generator." + "We are planning to compare the results of the numpy, namba and IDP dpnp so we need to make sure that the results are reproducible. We can do this through the use of a random seed function that initializes a random number generator." ] }, { @@ -370,7 +370,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Like before, let's measure the accuracy of the prepared implementation. It is measured as the number of well-assigned classes for the test set. The final result is the same for all: NumPy, numba and numba-dpex implementations." + "Like before, let's measure the accuracy of the prepared implementation. It is measured as the number of well-assigned classes for the test set. The final result is the same for all: NumPy, numba and dpnp implementations." ] }, { diff --git a/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpnp_kNN/IntelPython_Numpy_Numba_dpnp_kNN.py b/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpnp_kNN/IntelPython_Numpy_Numba_dpnp_kNN.py index b0cd41ea4c..024c534672 100644 --- a/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpnp_kNN/IntelPython_Numpy_Numba_dpnp_kNN.py +++ b/AI-and-Analytics/Features-and-Functionality/IntelPython_Numpy_Numba_dpnp_kNN/IntelPython_Numpy_Numba_dpnp_kNN.py @@ -11,10 +11,10 @@ # ============================================================= -# # Simple k-NN classification with numba_dpex IDP optimization -# -# This sample shows how to receive the same accuracy of the k-NN model classification by using numpy, numba and numba_dpex. The computation are performed using wine dataset. -# +# # Simple k-NN classification with Data Parallel Extension for NumPy IDP optimization +# +# This sample shows how to receive the same accuracy of the k-NN model classification by using numpy, numba and dpnp. The computation are performed using wine dataset. +# # Let's start with general imports used in the whole sample. # In[ ]: @@ -27,11 +27,11 @@ # ## Data preparation -# +# # Then, let's download the dataset and prepare it for future computations. -# +# # We are using the wine dataset available in the sci-kit learn library. For our purposes, we will be using only 2 features: alcohol and malic_acid. -# +# # So first we need to load the dataset and create DataFrame from it. Later we will limit the DataFrame to just target and 2 classes we choose for this problem. # In[ ]: @@ -51,7 +51,7 @@ df.head() -# We are planning to compare the results of the numpy, namba and IDP numba_dpex so we need to make sure that the results are reproducible. We can do this through the use of a random seed function that initializes a random number generator. +# We are planning to compare the results of the numpy, namba and IDP dpnp so we need to make sure that the results are reproducible. We can do this through the use of a random seed function that initializes a random number generator. # In[ ]: @@ -60,7 +60,7 @@ # The next step is to prepare the dataset for training and testing. To do this, we randomly divided the downloaded wine dataset into a training set (containing 90% of the data) and a test set (containing 10% of the data). -# +# # In addition, we take from both sets (training and test) data *X* (features) and label *y* (target). # In[ ]: @@ -78,9 +78,9 @@ # ## NumPy k-NN -# +# # Now, it's time to implement the first version of k-NN function using NumPy. -# +# # First, let's create simple euclidean distance function. We are taking positions form the provided vectors, counting the squares of the individual differences between the positions, and then drawing the root of their sum for the whole vectors (remember that the vectors must be of equal length). # In[ ]: @@ -93,7 +93,7 @@ def distance(vector1, vector2): # Then, the k-nearest neighbors algorithm itself. -# +# # 1. We are starting by defining a container for predictions the same size as a test set. # 2. Then, for each row in the test set, we calculate distances between then and every training record. # 3. We are sorting training datasets based on calculated distances @@ -145,11 +145,11 @@ def knn(X_train, y_train, X_test, k): # ## Numba k-NN -# +# # Now, let's move to the numba implementation of the k-NN algorithm. We will start the same, by defining the distance function and importing the necessary packages. -# +# # For numba implementation, we are using the core functionality which is `numba.jit()` decorator. -# +# # We are starting with defining the distance function. Like before it is a euclidean distance. For additional optimization we are using `np.linalg.norm`. # In[ ]: @@ -157,7 +157,6 @@ def knn(X_train, y_train, X_test, k): import numba - @numba.jit(nopython=True) def euclidean_distance_numba(vector1, vector2): dist = np.linalg.norm(vector1 - vector2) @@ -174,6 +173,7 @@ def knn_numba(X_train, y_train, X_test, k): # 1. Prepare container for predictions predictions = np.zeros(X_test.shape[0]) for x in np.arange(X_test.shape[0]): + # 2. Calculate distances inputs = X_train.copy() distances = np.zeros((inputs.shape[0], 1)) @@ -198,7 +198,7 @@ def knn_numba(X_train, y_train, X_test, k): counter = {} for item in neighbor_classes: if item in counter: - counter[item] = counter.get(item) + 1 + counter[item] += 1 else: counter[item] = 1 counter_sorted = sorted(counter) @@ -208,8 +208,8 @@ def knn_numba(X_train, y_train, X_test, k): return predictions -# Similarly, as in the NumPy example, we are testing implemented method for the `k = 3`. -# +# Similarly, as in the NumPy example, we are testing implemented method for the `k = 3`. +# # The accuracy of the method is the same as in the NumPy implementation. # In[ ]: @@ -222,132 +222,49 @@ def knn_numba(X_train, y_train, X_test, k): print("Numba accuracy:", accuracy) -# ## Numba_dpex k-NN -# -# Numba_dpex implementation use `numba_dpex.kernel()` decorator. For more information about programming, SYCL kernels go to: https://intelpython.github.io/numba-dpex/latest/user_guides/kernel_programming_guide/index.html. -# -# Calculating distance is like in the NumPy example. We are using Euclidean distance. Later, we create the queue of the neighbors by the calculated distance and count in provided *k* votes for dedicated classes of neighbors. -# -# In the end, we are taking a class that achieves the maximum value of votes and setting it for the current global iteration. +# ## Data Parallel Extension for NumPy k-NN +# +# To take benefit of DPNP, we can leverage its vectorized operations and efficient algorithms to implement a k-NN algorithm. We will use optimized operations like `sum`, `sqrt` or `argsort`. +# +# Calculating distance is like in the NumPy example. We are using Euclidean distance. The next step is to find the indexes of k-nearest neighbours for each test poin, and get tehir labels. At the end, we neet to determine the most frequent label among k-nearest. # In[ ]: -import numba_dpex - - -@numba_dpex.kernel -def knn_numba_dpex( - item: numba_dpex.kernel_api.Item, - train, - train_labels, - test, - k, - predictions, - votes_to_classes_lst, -): - dtype = train.dtype - i = item.get_id(0) - queue_neighbors = numba_dpex.kernel_api.PrivateArray(shape=(3, 2), dtype=dtype) +import dpnp as dpnp - for j in range(k): - x1 = train[j, 0] - x2 = test[i, 0] +def knn_dpnp(train, train_labels, test, k): + # 1. Calculate pairwise distances between test and train points + distances = dpnp.sqrt(dpnp.sum((test[:, None, :] - train[None, :, :])**2, axis=-1)) - distance = dtype.type(0.0) - diff = x1 - x2 - distance += diff * diff - dist = math.sqrt(distance) + # 2. Find the indices of the k nearest neighbors for each test point + nearest_neighbors = dpnp.argsort(distances, axis=1)[:, :k] - queue_neighbors[j, 0] = dist - queue_neighbors[j, 1] = train_labels[j] + # 3. Get the labels of the nearest neighbors + nearest_labels = train_labels[nearest_neighbors] - for j in range(k): - new_distance = queue_neighbors[j, 0] - new_neighbor_label = queue_neighbors[j, 1] - index = j + # 4. Determine the most frequent label among the k nearest neighbors + unique_labels, counts = np.unique(nearest_labels, return_counts=True) + predicted_labels = nearest_labels[np.argmax(counts)] - while index > 0 and new_distance < queue_neighbors[index - 1, 0]: - queue_neighbors[index, 0] = queue_neighbors[index - 1, 0] - queue_neighbors[index, 1] = queue_neighbors[index - 1, 1] - - index = index - 1 - - queue_neighbors[index, 0] = new_distance - queue_neighbors[index, 1] = new_neighbor_label - - for j in range(k, len(train)): - x1 = train[j, 0] - x2 = test[i, 0] - - distance = dtype.type(0.0) - diff = x1 - x2 - distance += diff * diff - dist = math.sqrt(distance) - - if dist < queue_neighbors[k - 1, 0]: - queue_neighbors[k - 1, 0] = dist - queue_neighbors[k - 1, 1] = train_labels[j] - new_distance = queue_neighbors[k - 1, 0] - new_neighbor_label = queue_neighbors[k - 1, 1] - index = k - 1 - - while index > 0 and new_distance < queue_neighbors[index - 1, 0]: - queue_neighbors[index, 0] = queue_neighbors[index - 1, 0] - queue_neighbors[index, 1] = queue_neighbors[index - 1, 1] - - index = index - 1 - - queue_neighbors[index, 0] = new_distance - queue_neighbors[index, 1] = new_neighbor_label - - votes_to_classes = votes_to_classes_lst[i] - - for j in range(len(queue_neighbors)): - votes_to_classes[int(queue_neighbors[j, 1])] += 1 - - max_ind = 0 - max_value = dtype.type(0) - - for j in range(3): - if votes_to_classes[j] > max_value: - max_value = votes_to_classes[j] - max_ind = j - - predictions[i] = max_ind + return predicted_labels # Next, like before, let's test the prepared k-NN function. -# -# In this case, we will need to provide the container for predictions: `predictions` and the container for votes per class: `votes_to_classes_lst` (the container size is 3, as we have 3 classes in our dataset). -# -# We are running a prepared k-NN function on a CPU device as the input data was allocated on the CPU. Numba-dpex will infer the execution queue based on where the input arguments to the kernel were allocated. Refer: https://intelpython.github.io/oneAPI-for-SciPy/details/programming_model/#compute-follows-data -# In[ ]: - +# +# We are running a prepared k-NN function on a CPU device as the input data was allocated on the CPU using DPNP. -import dpnp +# In[ ]: -predictions = dpnp.empty(len(X_test.values), device="cpu") -# we have 3 classes -votes_to_classes_lst = dpnp.zeros((len(X_test.values), 3), device="cpu") X_train_dpt = dpnp.asarray(X_train.values, device="cpu") y_train_dpt = dpnp.asarray(y_train.values, device="cpu") X_test_dpt = dpnp.asarray(X_test.values, device="cpu") -numba_dpex.call_kernel( - knn_numba_dpex, - numba_dpex.Range(len(X_test.values)), - X_train_dpt, - y_train_dpt, - X_test_dpt, - 3, - predictions, - votes_to_classes_lst, -) +pred = knn_dpnp(X_train_dpt, y_train_dpt, X_test_dpt, 3) -# Like before, let's measure the accuracy of the prepared implementation. It is measured as the number of well-assigned classes for the test set. The final result is the same for all: NumPy, numba and numba-dpex implementations. +# Like before, let's measure the accuracy of the prepared implementation. It is measured as the number of well-assigned classes for the test set. The final result is the same for all: NumPy, numba and dpnp implementations. # In[ ]: @@ -355,10 +272,11 @@ def knn_numba_dpex( predictions_numba = dpnp.asnumpy(predictions) true_values = y_test.to_numpy() accuracy = np.mean(predictions_numba == true_values) -print("Numba_dpex accuracy:", accuracy) +print("Data Parallel Extension for NumPy accuracy:", accuracy) # In[ ]: -print("[CODE_SAMPLE_COMPLETED_SUCCESSFULLY]") +print("[CODE_SAMPLE_COMPLETED_SUCCESFULLY]") + From 65340c6f0d4c4aaa22be324f36f6cd030e2b50f3 Mon Sep 17 00:00:00 2001 From: Rakshith Krishnappa Date: Thu, 23 Jan 2025 20:50:25 +0000 Subject: [PATCH 6/7] updated LNL and BMG device details --- Tools/GPU-Occupancy-Calculator/index.html | 168 ++++++++++++++++++++-- 1 file changed, 153 insertions(+), 15 deletions(-) diff --git a/Tools/GPU-Occupancy-Calculator/index.html b/Tools/GPU-Occupancy-Calculator/index.html index 9b469a8422..52066a8e98 100644 --- a/Tools/GPU-Occupancy-Calculator/index.html +++ b/Tools/GPU-Occupancy-Calculator/index.html @@ -235,12 +235,48 @@

Disclaimer