{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Báo Cáo Bài Tập Lớn Học Máy Cuối Kỳ\n* **Họ và tên:** Dương Quốc Cường\n* **Mã số sinh viên:** 18020254\n* **Mã lớp:** INT3405_1","metadata":{}},{"cell_type":"markdown","source":"# Workflow\n1. <a href=\"#1.-Problem-description\">Problem description</a>\n2. <a href=\"#2.-Data-preprocessing\">Data preprocessing</a>\n    * <a href=\"#Cleaning-Data\">Cleaning Data</a>\n    * <a href=\"#Vectorization\">Vectorization</a>\n3. <a href=\"#3.-Training\">Training</a>\n    * <a href=\"#Naive-Bayes\">Naive Bayes</a>\n    * <a href=\"#Logistic-Regression\">Logistic Regression</a>\n    * <a href=\"#LSTM\">LSTM</a>\n4. <a href=\"#4.-Conclution\">Conclution</a>\n5. <a href=\"#5.-Result\">Result</a>","metadata":{}},{"cell_type":"markdown","source":"# 1. Problem description\n>Trong bài toán này, ta cần phân loại các câu hỏi xem liệu chúng có phải là toxic question hay không. Đây là một bài toán khá đơn giản với 2 lớp, lớp is insincere(1) và sincere(0) \n* Đầu vào của bài toán là dữ liệu dạng text\n* Đầu ra của bài toán là [\"yes\":1, \"no\":0] \n        => Đây là một bài toán Binari Classifier","metadata":{}},{"cell_type":"markdown","source":"Để đánh giá mô hình, ở đây ta dùng f1_score do dữ liệu đang bị mất cân bằng (<a href=\"#Cleaning-Data\">được giải thích tại đây</a>) và đây là một chỉ số thay thế lý tưởng cho accuracy khi mô hình có tỷ lệ mất cân bằng mẫu cao.<br>\n- f1-score là rung bình điều hòa giữa Precision và Recall $$\\frac{1}{\\frac{1}{precision} + \\frac{1}{recall}}$$\nVới:\n> Precision: Mức độ dự báo chính xác trong những trường hợp được dự báo là Positive.<br>$$precision=\\frac{TP}{TP + FP}$$\n Recall: Mức độ dự báo chuẩn xác những trường hợp là Positive trong những trường hợp thực tế là Positive. $$recall=\\frac{TP}{TP + FN}$$\nvà có thể được biểu diễn như hình sau: <br>\n![](https://www.kdnuggets.com/images/precision-recall-relevant-selected.jpg)","metadata":{}},{"cell_type":"code","source":"import numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\nimport os\nimport time\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n        \n# Vẽ\nimport matplotlib.pyplot as plt\n\nimport sys\nimport warnings\n\nif not sys.warnoptions:\n    warnings.simplefilter(\"ignore\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Load data dùng để huấn luyện model\nĐể xây dựng một mô hình ta cần phải có một tập dữ liệu.$$$$\nTa load dữ liệu đã cho, sau đó:\n * Kiểm tra các thành phần trong tập dữ liệu\n * Kiểm tra lượng dữ liệu có trong các tập","metadata":{}},{"cell_type":"code","source":"train_df = pd.read_csv('/kaggle/input/quora-insincere-questions-classification/train.csv')\ntest_df = pd.read_csv('/kaggle/input/quora-insincere-questions-classification/test.csv')\ntrain_df.info()\nprint(\"Train: \", train_df.shape)\nprint(\"Test: \", test_df.shape)\ntrain_df.head()","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"test_df.head()","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Kết quả: Các câu hỏi trong tập train và test đều khá tương đồng. Tập training data có khoảng 1,3 triệu câu hỏi và tập test có khoảng 400k <br>\nTừ 5 dòng đầu tiên trong tập data ta được biết:\n* qid là id của câu hỏi \n* question_text chứa nội dung câu hỏi => dữ liệu chính cần xử lý để huấn luyện mô hình\n* Đối với tập train, target là label của câu hỏi đó(1 nếu insincere(sau này tạm gọi là câu hỏi độc hại) và 0 với sincere(hữu ích))","metadata":{}},{"cell_type":"markdown","source":"# 2. Data preprocessing\nTrong phần trên, ta đã hiểu sơ qua về dữ liệu dùng để huấn luyện, nhưng như vậy là chưa đủ.\n    => Ta cần tìm hiểu thêm để bắt đầu đi vào xử lý","metadata":{}},{"cell_type":"markdown","source":"Thêm một số thư viện xử lý ngôn ngữ tự nhiên","metadata":{}},{"cell_type":"code","source":"import unidecode\nimport re\nimport nltk\nimport string\nimport codecs\nimport spacy\nfrom nltk.corpus import stopwords\nfrom nltk.stem import WordNetLemmatizer\nimport pickle","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Cleaning Data","metadata":{}},{"cell_type":"markdown","source":"Để xử lý dữ liệu, ta cần loại bỏ các sữ liệu bị mất, không xác định. <br>  \nTa sẽ kiểm tra các giá trị NULL, N/A có trong tập dữ liệu, nếu có thì sẽ loại bỏ","metadata":{}},{"cell_type":"code","source":"print('Số giá trị NULL trong tập train = %d' % (train_df['question_text'].isnull().sum()))\nprint('Số giá trị N/A trong tập train = %d' % (train_df['question_text'].isna().sum()))\n\nprint('Số giá trị NULL trong tập test = %d' % (test_df['question_text'].isnull().sum()))\nprint('Số giá trị N/A trong tập test = %d' % (test_df['question_text'].isna().sum()))\n# Không có giá trị nào","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"> Trong cả 2 tập train và test đều không có.","metadata":{}},{"cell_type":"markdown","source":"* Tính cân bằng của tập dữ liệu ảnh hưởng rất lớn đến quá trình train, và trong bài toán này việc dự đoán được đúng câu hỏi độc hại hay không lại quan trọng hơn việc dự đoán đúng câu hỏi hữu ích. Thế nên ta cần phải biết được mức độ cân bằng của tập dữ liệu. <br>\n* Ta sẽ kiểm tra tỷ lệ dữ liệu training theo nhãn để biết được điều này:","metadata":{}},{"cell_type":"code","source":"import collections\n\ntarget = train_df['target']\ntarget_1 = 0\nfor target_value in target:\n    if target_value == 1:\n        target_1 += 1\nprint(\"Số câu hỏi trong tệp train:\", len(target))\nprint(\"Số câu hỏi độc hại là:\", target_1)\n\nfig, ax = plt.subplots(1,1, figsize=(7,7))\nax.set_title(\"Tỷ lệ các câu hỏi đã được dán nhãn\")\nexplode=(0,0.1)\nlabels ='Hữu ích','Độc hại'\ncolors = ['#00ff00', '#ff0000']\n\ncounts = list(dict(collections.Counter(list(train_df.target))).values()) \nax.pie(counts , explode=explode, labels=labels, colors=colors, autopct='%1.1f%%',shadow=True, startangle=90)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"> Đánh giá: Như ta thấy phần lớn các câu hỏi là hữu ích (93,8%) và chỉ có một phần nhỏ các câu hỏi là độc hại. Dữ liệu này bị mất cân bằng rất nghiêm trọng nên khi đánh giá chất lượng mô hình mà ta áp dụng:\n* Ta sẽ ít tin tưởng vào accuracy score khi đánh giá. \n* Tìm hướng xử lý để giải quyết tình trạng này (VD: Thay đổi metrix, OverSampling, UnderSampling, SMOTE...v..v).","metadata":{}},{"cell_type":"markdown","source":"Ta sẽ xem một phần dữ liệu hiện tại để tìm hiểu cũng như tìm hướng xử lý <br>","metadata":{}},{"cell_type":"code","source":"for i in range(5):\n    print(train_df['question_text'][i])","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":" Tiếp tục dùng WordCloud để kiểm tra tần suất một số từ thường xuyên xuất hiện trong tập dữ liệu.","metadata":{}},{"cell_type":"code","source":"from wordcloud import WordCloud\n\n# Funcion dùng để show wordcloud\ndef plot_cloud(wordcloud):\n    plt.figure(figsize=(10, 10))\n    plt.imshow(wordcloud) \n    plt.axis(\"off\");\n    plt.show();\n    \nprint('Các từ thường xuất hiện trong câu hỏi toxic')\ntext =\" \".join(train_df[train_df[\"target\"] == 1][\"question_text\"])\n\n # Tạo wordcloud \nwordcloud1 = WordCloud(width = 1024, height = 1024, random_state=1, background_color='white', colormap='rainbow', collocations=False).generate(text)\nplot_cloud(wordcloud1)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"print('Các từ thường xuất hiện trong câu hỏi chân thành')\ntext =\" \".join(train_df[train_df[\"target\"] == 0][\"question_text\"])\n\n # Tạo wordcloud \nwordcloud0 = WordCloud(width = 1024, height = 1024, random_state=1, background_color='white', colormap='rainbow', collocations=False).generate(text)\nplot_cloud(wordcloud0)","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Sau khi kiểm tra xong dữ liệu, ta có thể thấy dữ liệu đang khá phức tạp:\n   * Đầu tiên ta cần loại bỏ các khoảng trống, các ký tự đặc biệt\n   * Các từ dùng để hỏi xuất hiện ở mỗi câu, nhưng không suất hiện trên WordCloud => Không có ích khi train\n   * Các từ dừng (is, a, an,...) cũng cần được loại bỏ\n   * Các ký tự đặc biệt, các số thường xuất hiện nhiều nhưng không quan trọng trong quá trình train => loại bỏ\n   * Chuyển các từ rút gọn về dạng hoàn chỉnh\n   * Chuyển các từ viết hoa về chữ viết thường để dễ train\n   * Tìm các từ gốc của từ","metadata":{}},{"cell_type":"markdown","source":"Sau đây ta sẽ lần lượt giải quyết từng vấn đề này","metadata":{}},{"cell_type":"markdown","source":"**Chuyển các chữ viết hoa thành thường** <br>\nĐể dễ dàng train, cũng như giảm chiều của các vector đặc trưng sau này, việc chuyển đổi các chữ hoa sang thường là điều cần thiết <br>\nCách làm: \n* Ta sẽ tìm những chữ cái viết hoa và chuyển toàn bộ chúng thành chữ thường","metadata":{}},{"cell_type":"markdown","source":"**Chuyển các chữ viết hoa thành thường** <br>\nĐể dễ dàng train, cũng như giảm chiều của các vector đặc trưng sau này, việc chuyển đổi các chữ hoa sang thường là điều cần thiết <br>\nCách làm: \n* Ta sẽ tìm những chữ cái viết hoa và chuyển toàn bộ chúng thành chữ thường","metadata":{}},{"cell_type":"markdown","source":"**Chuyển các từ viết tắt về dạng đầy đủ**<br>\nTrong quá trình train, các từ viết tắt đôi khi làm nhiễu thông tin, cũng như tạo ra các chiều vector thừa thãi, không có ích, ta cần phải đưa chúng về dạng hoàn chỉnh <br>\nCách làm:\n* Đầu tiên, ta khai báo một dict với các từ viết tắt và dạng hoàn chỉnh của nó\n* Tiếp theo, ta sẽ decode để tìm ra các dấu nháy đơn nhằm tìm các từ viết tắt, sau đó mapping chúng về dạng đúng","metadata":{}},{"cell_type":"markdown","source":"**Xóa các từ dừng, các từ dùng để đặt câu hỏi** <br>\nCác từ dừng trong tiếng anh (is, a, an, ...) thường xuất hiện khá nhiều nhưng lại không quá quan trọng trong việc train, những câu dùng để đặt câu hỏi cũng vậy, ta sẽ loại bỏ chúng\n<br>Cách làm: \n* ta khai báo các từ đó, tìm chúng trong câu và loại bỏ, thư viện 'nltk' đã cung cấp cho chúng ta tất cả những thứ cần phải loại bỏ này.<br>","metadata":{}},{"cell_type":"code","source":"stop_words = set(stopwords.words('english'))\n\ncontraction_mapping = {\"ain't\": \"is not\", \"aren't\": \"are not\",\"can't\": \"cannot\", \"can't've\": \"cannot have\", \"'cause\": \"because\", \"could've\": \"could have\", \"couldn't\": \"could not\", \"couldn't've\": \"could not have\",\"didn't\": \"did not\",  \"doesn't\": \"does not\", \"don't\": \"do not\", \"hadn't\": \"had not\", \"hadn't've\": \"had not have\", \"hasn't\": \"has not\", \"haven't\": \"have not\",  \"he'd\": \"he would\", \"he'd've\": \"he would have\", \"he'll\": \"he will\", \"he'll've\": \"he will have\", \"he's\": \"he is\", \"how'd\": \"how did\", \"how'd'y\": \"how do you\", \"how'll\": \"how will\", \"how's\": \"how is\",  \"I'd\": \"I would\", \"I'd've\": \"I would have\", \"I'll\": \"I will\", \"I'll've\": \"I will have\",\"I'm\": \"I am\", \"I've\": \"I have\", \"i'd\": \"i would\", \"i'd've\": \"i would have\", \"i'll\": \"i will\", \"i'll've\": \"i will have\",\"i'm\": \"i am\", \"i've\": \"i have\", \"isn't\": \"is not\", \"it'd\": \"it would\", \"it'd've\": \"it would have\", \"it'll\": \"it will\", \"it'll've\": \"it will have\",\"it's\": \"it is\", \"let's\": \"let us\", \"ma'am\": \"madam\", \"mayn't\": \"may not\", \"might've\": \"might have\",\"mightn't\": \"might not\",\"mightn't've\": \"might not have\", \"must've\": \"must have\", \"mustn't\": \"must not\", \"mustn't've\": \"must not have\", \"needn't\": \"need not\", \"needn't've\": \"need not have\",\"o'clock\": \"of the clock\", \"oughtn't\": \"ought not\", \"oughtn't've\": \"ought not have\", \"shan't\": \"shall not\",\"sha'n't\": \"shall not\", \"shan't've\": \"shall not have\", \"she'd\": \"she would\", \"she'd've\": \"she would have\", \"she'll\": \"she will\", \"she'll've\": \"she will have\", \"she's\": \"she is\", \"should've\": \"should have\", \"shouldn't\": \"should not\", \"shouldn't've\": \"should not have\", \"so've\": \"so have\",\"so's\": \"so as\", \"this's\": \"this is\",\"that'd\": \"that would\", \"that'd've\": \"that would have\",\"that's\": \"that is\", \"there'd\": \"there would\", \"there'd've\": \"there would have\",\"there's\": \"there is\", \"here's\": \"here is\",\"they'd\": \"they would\", \"they'd've\": \"they would have\", \"they'll\": \"they will\", \"they'll've\": \"they will have\", \"they're\": \"they are\", \"they've\": \"they have\", \"to've\": \"to have\", \"wasn't\": \"was not\", \"we'd\": \"we would\", \"we'd've\": \"we would have\", \"we'll\": \"we will\", \"we'll've\": \"we will have\", \"we're\": \"we are\", \"we've\": \"we have\", \"weren't\": \"were not\", \"what'll\": \"what will\", \"what'll've\": \"what will have\", \"what're\": \"what are\", \"what's\": \"what is\", \"what've\": \"what have\", \"when's\": \"when is\", \"when've\": \"when have\", \"where'd\": \"where did\", \"where's\": \"where is\", \"where've\": \"where have\", \"who'll\": \"who will\", \"who'll've\": \"who will have\", \"who's\": \"who is\", \"who've\": \"who have\", \"why's\": \"why is\", \"why've\": \"why have\", \"will've\": \"will have\", \"won't\": \"will not\", \"won't've\": \"will not have\", \"would've\": \"would have\", \"wouldn't\": \"would not\", \"wouldn't've\": \"would not have\", \"y'all\": \"you all\", \"y'all'd\": \"you all would\",\"y'all'd've\": \"you all would have\",\"y'all're\": \"you all are\",\"y'all've\": \"you all have\",\"you'd\": \"you would\", \"you'd've\": \"you would have\", \"you'll\": \"you will\", \"you'll've\": \"you will have\", \"you're\": \"you are\", \"you've\": \"you have\" } \n\nfind_lem = WordNetLemmatizer()","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def clean_text(text):\n    \n     # Decode dữ liệu dạng text\n    try:\n        decoded = unidecode.unidecode(codecs.decode(text, 'unicode_escape'))\n    except:\n        decoded = unidecode.unidecode(text)\n        \n    # Khi xuất hiện dấu nháy đơn, ta bắt đầu xử lý các chữ viết tắt\n    escape_handled = re.sub(\"’\", \"'\", decoded)\n    text = ' '.join([contraction_mapping[t] if t in contraction_mapping else t for t in escape_handled.split(\" \")])\n    \n    # Đổi các ký tự viết hoa sang thường\n    text = re.findall(r\"[a-zA-Z]+\", text.lower())\n    \n    text = [word for word in text if (word not in stop_words and len(word)>2)]\n    \n    # Tìm từ gốc của tất cả các từ và chuyển đổi\n    text = [find_lem.lemmatize(word) for word in text]\n    \n    text = re.sub(r'(.)\\1+', r'\\1\\1', ' '.join(text))\n    \n    return text","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Ta sẽ thử clean 1 đoạn dữ liệu và in ra để kiểm tra kết quả sau khi clean.","metadata":{}},{"cell_type":"code","source":"t_cleaned = \"How did Quebec D nationalists # #$%@&*()+_-;:<=>.?{} * Aren't can't've see C 2 ain't their F6 province as a nation in the 1960s?\"\n\nprint('Chưa clean: ' + t_cleaned)\nprint('Sau khi clean: ' + clean_text(t_cleaned))","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"> Đánh giá: như vậy dữ liệu đã được clean thành dạng khá đẹp, ta có thể bắt đầu clean các tập dữ liệu train và test<br>","metadata":{}},{"cell_type":"code","source":"train_cleaned = train_df['question_text'].apply(lambda x: clean_text(x))\ntest_cleaned = test_df['question_text'].apply(lambda x: clean_text(x))\n\n# pickle.dump(train_df['question_text_cleaned'], open(\"train-cleaned.pickle\", \"wb\"))\n# pickle.dump(test_df['question_text_cleaned'], open(\"test-cleaned.pickle\", \"wb\"))\n\n# train_cleaned = pickle.load(open(os.path.join('/kaggle/input/question-cleaned', 'train-cleaned.pickle'), 'rb'))\n# test_cleaned = pickle.load(open(os.path.join('/kaggle/input/question-cleaned', 'test-cleaned.pickle'), 'rb'))\n","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"> Đánh giá: Quá trình cleaned mất khá nhiều thời gian, ở đây sau khi clean ta tải dữ liệu lên input của bài để dễ dàng sử dụng sau này.","metadata":{}},{"cell_type":"markdown","source":"Tách dữ liệu để đánh giá mô hình.<br>\nTa sử dụng hàm chia dữ liệu đã được cho trước trong thư viện sklearn","metadata":{}},{"cell_type":"code","source":"from sklearn.model_selection import train_test_split\n\nX_train, X_test, y_train, y_test = train_test_split(train_cleaned, train_df['target'], test_size=0.2, random_state=1)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"**Tokenizer**<br>\nChúng ta cần tạo ra một từ điển mapping mỗi từ với index tương ứng của nó để tiện cho việc tạo ra ma trận số. Module tokenizer dễ dàng giúp ta thực hiện việc này.","metadata":{}},{"cell_type":"code","source":"from tensorflow.keras.preprocessing.text import Tokenizer\nfrom tensorflow.keras.preprocessing import sequence\n\n#max_words: Số lượng từ lớn nhất xuất hiện trong tokenizer được lấy từ tần suất xuất hiện trong văn bản từ cao xuống thấp.\nmax_words=50000\n#max_len: Số lượng các từ lớn nhất trong một câu hỏi.\nmax_len=100\n\ntokenizer = Tokenizer(num_words=max_words)\ntokenizer.fit_on_texts(list(train_cleaned))\n\ntrain_tok =  tokenizer.texts_to_sequences(X_train)\ntest_tok =  tokenizer.texts_to_sequences(X_test)\n\ntest_res_tok = tokenizer.texts_to_sequences(test_cleaned)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Vectorization\n>Trước khi đưa vào huấn luyện ta đã làm sạch dữ liệu một cách tương đối, nhưng để đưa vào mô hình dự đoán này, ta cần phải mã hóa dữ liệu dưới dạng số.","metadata":{}},{"cell_type":"markdown","source":"Phương pháp phổ biến đầu tiên ta có thể dùng để đưa các dữ liệu dạng text về các vector mà trong đó mỗi phần tử là một số: 'Bags of words'","metadata":{}},{"cell_type":"markdown","source":"CountVectorize sẽ biến tập dữ liệu train thành một ma trận với:\n* Số hàng ứng với số câu hỏi có trong tập data train, số cột ứng với số từ trong tập\n* Trong một hàng, giá trị của các cột thể hiện số lượng các từ mà hàng đó có (0->N)<br>\n> Trong tập test ta chỉ cần đếm nó theo ma trận đã có ở tập train","metadata":{}},{"cell_type":"markdown","source":"> Cách làm như trên là cách đơn giản nhất, tuy vậy không phải trong tất cả các câu hỏi cần phân loại cũng đều có hết những từ thuộc tập train, nên ma trận sinh ra, hay cụ thể hơn là các vector của mỗi câu hỏi sẽ là những sparse vector và các vector đặc trưng này cũng rất dài, đây là một nhược điểm của phương pháp này, chúng ta có thể cải tiến bằng cách sử dụng phương pháp TF-IDF.","metadata":{}},{"cell_type":"markdown","source":"Dễ thấy, đôi khi có những từ rất ít khi xuất hiện, nhưng lại là thông tin quan trọng dùng để phân loại, và cũng có những từ xuất hiện rất nhiều nhưng lại không quá quan trọng khi training, nên phương pháp TF-IDF có thể được sử dụng. $$$$\nPhương pháp TF-IDF sẽ đánh giá tầm quan trọng của một từ trong một câu hỏi dựa trên toàn bộ tập dữ liệu với độ quan trọng được tính bằng công thức: \n                      $$tf(w, ques) * log(\\frac{n}{n_{w} + 1})$$\n* $tf(w, ques)$: chính là tần suất xuất hiện của một từ trong một câu hỏi: *(số lần xuất hiện trong câu hỏi) / (từ xuất hiện nhiều nhất trong câu)*\n* $n$: là tổng số các câu hỏi trong tập\n* $n_{w}$: là số lần từ này xuất hiện trong toàn bộ tập","metadata":{}},{"cell_type":"code","source":"from sklearn.feature_extraction.text import TfidfVectorizer\n\ntfidf = TfidfVectorizer(ngram_range=(1, 3), max_df=0.9)\n\ntfidf.fit(train_cleaned)\n\ntraining_features_tfidf = tfidf.transform(X_train)\ntest_features_tfidf = tfidf.transform(X_test)\n\nsmpl_vect = tfidf.transform(train_cleaned.sample(1, random_state = 1))\nsmpl_vect.toarray()","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Đánh giá: Như vậy một từ phổ biết sẽ có idf càng nhỏ và tf-idf càng lớn\nỞ đây ta còn sử dụng thêm tham số N-gram để chọn thêm các chiều của từ cũng như tham số max_df để loại bỏ những từ mà ta sẽ coi nó như một stopword khi nó xuất hiện quá nhiều trong dataset (ở đây ta cho là 90%) và không có ích.","metadata":{}},{"cell_type":"markdown","source":"# 3. Training\n> Trong các bài toán phân loại nhị phân như thế này, những mô hình đơn giản đầu tiên ta có thể nghĩ đến như Logistic Regression, native bayes, cây quyết định ...v...v. Trong quá trình train sau đây ta sử dụng 3 mô hình:\n* Naive Bayes\n* Logistic Regression\n* LSTM","metadata":{}},{"cell_type":"code","source":"from sklearn import metrics\nfrom sklearn.metrics import accuracy_score, f1_score","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Naive Bayes","metadata":{}},{"cell_type":"markdown","source":"Khi nói đến bài toán phân lớp cho dữ liệu NLP, mô hình đầu tiên ta có thể nghĩ ngay đến là Naive Bayes Classifiers, bởi vì đây là một thuật toán phân lớp có độ hiệu quả, tính chính xác cao và thời gian huấn luyện mô hình nhỏ.","metadata":{}},{"cell_type":"markdown","source":"Thuật toán Naive Bayes Classifiers(NBC) được dựa trên định lý Bayes, nó cho phép chúng ta tính được xác suất của biến cố A khi biến cố B sảy ra trước:\n$$P(B|A) = \\frac{P(A|B)P(B)}{P(A)}$$\nVới D là tập các câu hỏi huấn luyện, trong đó mỗi câu hỏi A chứa n giá trị thuộc tính B1,B2,...,Bn được biểu diễn bằng một vector n thành phần {x1,x2,...,xn} $$$$\nVới 2 lớp [0,1] và một câu hỏi A cho trước, bộ phân lớp sẽ phân cho A vào lớp [0] nếu: $P(A|C_{0}) > P(A|C_{1})$ và ngược lại. Vì P(A) là không đổi, xác suất $P(C_{i}) (i\\in [0,1])$ ta coi bằng nhau, và giả thuyết các xác suất là độc lập với nhau nên ta chỉ cầm tìm giá trị: $$P(A|C_{i})_{max} = P(x_{1}|C_{i}). ... .P(x_{n}|C_{n})$$\nTa có thể hiểu đơn giản hơn là ở đây ta tính xác suất câu hỏi A thuộc vào class C dựa trên việc tính, tìm xác suất lớn nhất mà nó thuộc vào trong mỗi class\n","metadata":{}},{"cell_type":"markdown","source":"Với mô hình Multinomial NB mà ta sử dụng, mỗi câu hỏi được biểu diễn bởi một vector có độ dài d chính là số từ trong tập data. Giá trị của thành phần thứ i trong mỗi vector chính là số lần từ thứ i xuất hiện trong văn bản đó (đã được giaỉ thích ở phần Tiền xử lý).$$$$\nKhi đó, $P(x_{i}|C)$ tỉ lệ với tần suất từ thứ i xuất hiện trong các văn bản của class C. Bài toán sẽ quy về việc tính xác suất của: $$P(x_{i}|C) = \\frac{N_{ci}}{N_{c}}$$\n\nThư vỉện sklearn đã cung cấp cho chúng ta mô hình này","metadata":{}},{"cell_type":"code","source":"from sklearn.naive_bayes import MultinomialNB\nnb_model = MultinomialNB(alpha=0.08)\nnb_model.fit(training_features_tfidf, y_train)\nnb_predict = nb_model.predict(test_features_tfidf)\n\n# nb_model.fit(train_feature_tok, y_train)\n# nb_predict = nb_model.predict(test_feature_tok)\n\n\nprint('Accuracy :', accuracy_score(y_test, nb_predict))\nprint('F1 score :', f1_score(y_test, nb_predict))","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"> Đánh giá: Nhìn chung, mô hình này là một mô hình khá là đơn giản.<br>\n* Vì Naive Bayes bị ảnh hưởng khá nhiều bởi dữ liệu mất cân bằng nên ảnh hưởng không tốt tới kết quả của mô hình.<br>\n* Mô hình này cũng sẽ đưa ra kết quả không tốt khi dữ liệu đầu vào chứa những từ không có trong tập train, xác suất dự đoán sẽ tiến gần tới không, để giải quyết điều này ta dùng kỹ thuật Laplace smoothing: $$P(x_{i}|C) = \\frac{N_{ci} + \\alpha }{N_{c} + d\\alpha }$$ để cho xác suất $P(x_{i}|C)$ khác không, sau nhiều lần thử thì hệ số alpha = 0.08 cho kết quả khá tốt đối với mô hình này\n* Tốc độ chạy của mô hình rất nhanh do việc tính toán rất đơn giản.","metadata":{}},{"cell_type":"markdown","source":"### Logistic Regression\n> Tiếp theo ta áp dụng một mô hình đơn giản nhưng lại là tiền đề cho mạng neural.","metadata":{}},{"cell_type":"markdown","source":"Cũng giống như các thuật toán Linear (tìm và tối ưu w) với công thức chung: $$y = f(w^{T}x),(w \\in [w_{0}, w_{1},... w_{n}])$$\nNhưng khác vơi các thuật toán như Perceptron Learning (sử dụng hàm sgn() để phân $w^{T}x$ vào 2 class) hay Linear Regression (tính trực tiếp và đưa ra kết quả của $w^{T}x$). <br>\nLogicstic Regression sử dụng các hàm kích hoạt (activation function) để biến đầu ra của bài toán thành xác suất mà câu hỏi sẽ rơi vào $C_{1}$ hay $C_{0}$. Lúc này bài toán có dạng: $$y = \\theta (w^{T}x)$$ \nVới theta($\\theta$) là hàm kích hoạt. Hàm kích hoạt ở đây có thể là (sigmoid, tanh, reLu,...), ở đây ta dùng hàm sigmoid, một hàm khá phổ biến với giá trị nằm trong đoạn từ 0 đến 1.<br>\n![img](https://www.researchgate.net/profile/Knut-Kvaal/publication/239269767/figure/fig2/AS:643520205430784@1530438581076/An-illustration-of-the-signal-processing-in-a-sigmoid-function.png)","metadata":{}},{"cell_type":"markdown","source":"Như vậy sau khi tính được phần tuyến tính là $w^{T}x$, phần tuyến tính này sẽ được đưa vào hàm sigmoid($\\sigma()$). Hàm $\\sigma()$ sẽ trả về xác suất mà câu hỏi là hữu ích ($y = 0$) hay độc hại ($y = 1$) với: $$P(y_{1}|x_{i};w) = \\sigma(w^{T}x); P(y_{0}|x_{i};w) = 1 - \\sigma(w^{T}x)$$\nBài toán quay trở về với việc **tối ưu hàm mất mát L** với mục tiêu làm cho $P(y|x_{i}) = \\sigma(w^{T}x) = \\widehat{y}$ càng ngày càng lớn (tiến gần tới 1): $$L = -log(P(y|x_{i}))$$ $$<=> \\frac{\\partial L}{\\partial w} = (\\widehat{y} - y)x = (\\sigma(w^{T}x) - y)x$$ $$=> w = w + \\lambda(\\widehat{y} - y)x$$\nVới $\\lambda$ là hệ học learning_rate (là một con số nhỏ thường nằm trong khoảng từ 0.01 đến 0.0001). <br>\nThư viện sklearn cũng đã tích hợp sẵn mô hình này nên ta sẽ lấy ra và sử dụng.","metadata":{}},{"cell_type":"code","source":"from sklearn.linear_model import LogisticRegression\n\nlogReg = LogisticRegression(solver='liblinear')\n\nlogReg.fit(training_features_tfidf, y_train)\nlogReg = nb_model.predict(test_features_tfidf)\n\n# nb_model.fit(train_feature_tok, y_train)\n# nb_predict = nb_model.predict(test_feature_tok)\n\n\nprint('Accuracy :', accuracy_score(y_test, nb_predict))\nprint('F1 score :', f1_score(y_test, nb_predict))","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Đánh giá:\n* Đây là một mô hình khá đơn giản với kết quả nhận được đều ở mức trung bình.\n* Tuy mô hình không yêu cầu dữ liêu là linearly separable, nhưng đường phân chia giữa 2 class vẫn là đường tuyến tính, nên cơ bản, bộ dữ liệu cũng phải rất gần với linearly separable, trong bài toán này, theo em mô hình sẽ hoạt động không tốt do dữ liệu là các từ mà đôi khi rất thường xuất hiện ở cả 2 class hữu ích và độc hại nên rất khó để mô hình có thể dự đoán.\n* Mô hình còn yêu cầu các điểm dữ liệu là độc lập với nhau nhưng trong bài toán này rất khó sảy ra, vì các từ cùng xuất hiện đôi khi ảnh hưởng lẫn nhau **=> Điều này dẫn đến việc áp dụng mạng neural thông thường với đầu vào và đầu ra độc lập với nhau không phù hợp với dữ liệu dạng này**.\n* Thuật toán cũng chạy chậm hơn so với NB vì mỗi lần cập nhật trọng số ta lại phải tính lại đạo hàm của Loss funcion với từng biến.<br>\nTa có thể giảm thời gian training của mô hình bằng cách sử dụng các mini batch nhưng ở mô hình này em sẽ không đề cập đến nó do em đang hướng đến một mô hình có kết quả tốt hơn.","metadata":{}},{"cell_type":"markdown","source":"### LSTM \n* Ta đã sử dụng một model cổ điển để giải quyết bài toán này, tiếp theo ta sẽ thử với một mô hình sâu hơn, cụ thể là mô hình LSTM, một mô hình được cải tiến từ mạng RNN.\n* Vì đây là một thuật toán DL mà nhiều chỗ em tìm hiểu chưa kĩ, nên em xin được phép KHÔNG trình bày các công thức tính toán hay tối ưu mà chỉ trình bày các ý tưởng mà mình đã hiểu cũng như cách sử dụng, và ứng dụng để xây dựng mô hình LSTM với các layer đơn giản.","metadata":{}},{"cell_type":"markdown","source":"**RNN**<br>\nRNN là một trong hai mạng neural lớn của Deep Learning và nó dùng để xử lý các dữ liệu dạng sequence hay time-series. Nên khi ứng dụng DL để xử lý bài toán dạng text này, ta sẽ nghĩ ngay đến RNN.<br>\n![](https://liyanxu.blog/wp-content/uploads/2019/01/Screen-Shot-2019-01-24-at-19.46.54.png)\n* Ý tưởng của RNN là sử dụng các dữ liệu đã được tính toán từ các bước xử lý trước để đưa ra dự đoán chính xác nhất cho bước hiện tại. <br>\n* Với hai đầu vào $x_{t}$ là dữ liệu đầu vào(câu hỏi) của state hiện tại, $h_{t - 1}$ là thông tin được lưu trữ ở state trước. $h_{t}$ là giá trị đầu ra dự đoán cũng như input của state tiếp theo. Activation funcion thường được sử dụng ở đây là tanh hoặc reLU.(h là hidden state.)<br>\n* Sau nhiều bước học và dự đoán qua các state, mạng sẽ cho ra kết quả cuối cùng được tổng hợp từ tất cả các input trước đó.\n> **Nhận xét:** Do vấn đề phụ thuộc xa, cụ thể là các input $h_{t - 1}$ của 1 state nhất định sẽ bị phụ thuộc vào các state trước đó, nên việc đạo hàm nhiều lần qua các state sẽ dẫn đến vanishing gradient khiến cho dữ liệu mà các state ở xa nhau nhận được sẽ bị mất mát, các câu hỏi được dự đoán sẽ chỉ lấy được các thông tin lưu trữ ở những state đủ gần với mình.<br>\nĐể giảm thiểu vấn đề này thì LSTM ra đời.","metadata":{}},{"cell_type":"markdown","source":"**LSTM**<br>\nLSTM có thể được mô hình hóa như sau:<br>\n![](https://i2.wp.com/nttuan8.com/wp-content/uploads/2019/06/lstm.png?resize=768%2C466&ssl=1)<br>\n* Như trực quan ta thấy LSTM tương đồng với RNN nhưng có thêm một điểm đặc biệt ở đầu vào là $c_{t - 1}$ và ở đầu ra là $c_{t}$.(ta gọi c là cell state)<br>\n* Như trong mô hình ta có thể thấy $c_{t}$ không đi qua quá nhiều bước tính toán, nó chỉ cần đi qua vài bước tính toán như element-wise multiplication, cộng ma trận để tổng hợp các thông tin quan trong từ $c_{t - 1}$ (cell state trước đó) và $h_{t - 1}$ (hidden state trước đó). Nên nó sẽ giảm được vanishing gradient và giúp lưu trữ được thông tin đi xa (long term memory).\n* Cách tính $\\widetilde{C_{t}}$ giống như cách tính $h_{t}$ trong RNN.(điểm tương tự)\n* Em sẽ không đi quá sâu vào phần giải thích các thành phần bên trong LSTM do vẫn chưa thể thật sự giải thích rõ chúng, và sẽ hiểu đơn giản đây là một mạng RNN được cải tiến bằng cách sử dụng cell state để trích suất một số lượng thông tin quan trọng từ các state trước, và với đặc tính lưu trữ được các thông tin state ở gần của RNN => ta gọi mô hình này là LSTM.","metadata":{}},{"cell_type":"markdown","source":"**Xây dựng một mô hình LSTM đơn giản**","metadata":{}},{"cell_type":"code","source":"from tensorflow.keras.layers import Input, Embedding, LSTM, Dense, Activation, Dropout, Flatten\nfrom tensorflow.keras.models import Model, Sequential\nfrom sklearn.preprocessing import LabelEncoder\nfrom tensorflow.keras.optimizers import RMSprop, Adam\nfrom tensorflow.keras.utils import to_categorical\nfrom tensorflow.keras.callbacks import EarlyStopping","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"* Đầu tiên ta cho dữ liệu tiến vào Embedding layer, layer này được khởi tạo với trọng số (weight) ngẫu nhiên và nó sẽ tìm hiểu cách embedded cho tất cả các từ trong tập dữ liệu training. *Embedding (input_dim: Kích thước của tập dữ liệu, output_dim: Kích thước không gian vector được embedding, input_length: Độ dài của câu hỏi)*\n* Sau đó ta cho chúng vào mạng LSTM.\n* Sử dụng Dense layer hay Fully-connected layer - một lớp cổ điển trong mạng nơ ron nhân tạo. Mỗi neural nhận đầu vào từ tất cả các output của mạng LSTM rồi đưa vào ReLU activation fuction để tính toán.\n* Ta sử dụng kỹ thuật Dropout để ngẫu nhiên loại bỏ p% số lượng node trong layer nhằm tránh việc model trở nên quá phức tạp, bị overfiting.\n* Sau khi dropout ta tiếp tục sử dụng Fully-connected layer để kết nối các node còn lại sau đó tính toán, đưa chúng vào sigmoid activation function và đưa ra kết quả.","metadata":{}},{"cell_type":"code","source":"def LSTM_Basic(maxword=50000, embedding_size=300, max_len=100, n_unit_lstm=64, n_unit_dense=64):\n    inputs = Input(name='inputs', shape=[max_len])\n    layer = Embedding(maxword, embedding_size, input_length=max_len)(inputs)\n\n    layer = LSTM(n_unit_lstm)(layer)\n    layer = Dense(n_unit_dense, name='FC1')(layer)\n    layer = Activation('relu')(layer)\n    layer = Dropout(0.4)(layer)\n    layer = Dense(1, name='out_layer')(layer)\n    layer = Activation('sigmoid')(layer)\n    model = Model(inputs=inputs,outputs=layer)\n    return model","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Ta bắt đầu khởi tạo mô hình, vì đây là bài toán phân loại nhị phân nên hàm ta dùng hàm loss là binary_crossentropy","metadata":{}},{"cell_type":"code","source":"lstm = LSTM_Basic()\nlstm.summary()\nlstm.compile(loss='binary_crossentropy', optimizer=Adam(), metrics=['accuracy'])","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"> Như vậy, ta đã xây dựng được một mô hình LSTM cơ bản với các layer như trên.","metadata":{}},{"cell_type":"markdown","source":"Ta bắt đầu công việc huấn luyện.<br>\nVì dữ liệu huấn luyện khá lớn và thuật toán ta dùng để tối ưu là GD, nên ta phải chia nhỏ chúng thành các mini_batch với size=512(vì thời gian khá lâu nên chỉ set epoch = 3).","metadata":{}},{"cell_type":"code","source":"# Do các pp như tf-idf hay countvectorizer khi chuyển các từ thành ma trận đều đã token các từ luôn\n# nhưng để sử dụng trong mô hình LSTM, ta đã có săn Embedding Layer nên không cần sử dụng đến chúng.\n\n# Padding dữ liệu đã tokenizer thành các chuỗi có độ dài bằng nhau\ntrain_feature_tok = sequence.pad_sequences(train_tok, maxlen=max_len)\ntest_feature_tok = sequence.pad_sequences(test_tok, maxlen=max_len)\ntest_res_tok = sequence.pad_sequences(test_res_tok, maxlen=max_len)\n\n# bắt đầu train\nlstm.fit(train_feature_tok, y_train, batch_size=512, epochs=2, validation_data=(test_feature_tok, y_test), verbose = 1)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"> Mô hình có hội tụ nhưng do chỉ sử dụng các layer đơn giản nên kết quả sẽ chỉ hội tụ đến một ngưỡng nào đó và chưa thể cho các kết quả tốt hơn (acuraccy trong khoảng 9.6-9.7).","metadata":{}},{"cell_type":"markdown","source":"Ta sử dụng tập val để thử dự đoán và tính các kết quả để tìm ra threshold hợp lý, từ đó đưa ra được chỉ số f1-score hợp lý cho mô hình","metadata":{}},{"cell_type":"code","source":"pred_val = lstm.predict([test_feature_tok], batch_size=1024, verbose=1)\nfor thresh in np.arange(0.1, 0.501, 0.01):\n    thresh = np.round(thresh, 2)\n    print(\"F1 score at threshold {0} is {1}\".format(thresh, metrics.f1_score(y_test, (pred_val>thresh).astype(int))))","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"> Ta có thể thấy rõ ràng kết quả của LSTM tốt hơn nhiều so với các mô hình còn lại","metadata":{}},{"cell_type":"markdown","source":"# 4. Conclution\n* Nhìn chung các mô hình đơn giản như Naive Bayes, Logistic Regression đều hoạt động khá ổn trên tập dataset.\n* Vì LSTM là một mô hình có chiều sâu hơn nên cho ra các kết quả tốt hơn so với các mô hình truyền thống, do thời gian nhiên cứu chưa lâu và chưa kỹ nên chưa áp dụng được các layer phổ biến như Time distributed Layer hay Attention Layer để mô hình cho ra các kết quả tốt hơn.\n* Tham khảo một số bài làm trên kaggle, để xử lý dữ liệu mất cân bằng, cụ thể là SMOTE thì kết quả train sau khi SMOTE cũng không được cải thiện so với khi không dùng:<br> [Smoted, but it did not work very well.](https://www.kaggle.com/stefanobromuri/smoted-but-it-did-not-work-very-well) <br> [Dealing with Class Imbalance with SMOTE](https://www.kaggle.com/theoviel/dealing-with-class-imbalance-with-smote)","metadata":{}},{"cell_type":"markdown","source":"# 5. Result","metadata":{}},{"cell_type":"markdown","source":"Mô hình LSTM có kết quả tốt nhất nên ta sẽ sử dụng để dự đoán\n> Vì kết quả đầu ra của LSTM được ta dùng hàm sigmoid nên kết quả trả về sẽ là xác suất mà câu hỏi là độc hại, nên ta sẽ chuyển đổi chúng thành one-hot với việc p > 50% sẽ là câu hỏi độc hại.","metadata":{}},{"cell_type":"code","source":"pred_t = lstm.predict([test_res_tok], batch_size=1024, verbose=1)\ntest_df['prediction'] = (np.array(pred_t) >= 0.5).astype(np.int)\nresult = test_df[['qid','prediction']]\nresult.set_index('qid', inplace=True)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"result.to_csv('submission.csv')","metadata":{"trusted":true},"execution_count":null,"outputs":[]}]}