Этим летом мне представилась возможность принять участие в микростажировках Open Avenues. В рамках своего проекта я использовал методы обработки естественного языка и машинного обучения для классификации отчетов медицинской радиологии. Этот блог официально подводит итоги проекта, над которым я работал последние 8 недель.
Обработка естественного языка
С популяризацией таких инструментов, как ChatGPT, обработка естественного языка (NLP) стала вызывать больший интерес в области компьютерных наук и искусственного интеллекта. Проще говоря, НЛП фокусируется на взаимодействии между компьютерами и естественным человеческим языком, с основной целью дать компьютерам возможность понимать, интерпретировать и генерировать человеческий язык.
В этом проекте я стремлюсь использовать обработку естественного языка (NLP) и методы машинного обучения, чтобы научить компьютеры классифицировать и прогнозировать категории разделов в образцах отчетов по медицинской радиологии.
МетаФлоу
Для проведения анализа в этом проекте будет использоваться MetaFlow.
MetaFlow, библиотека Python с открытым исходным кодом, созданная Netflix, предназначена для оптимизации и управления обширными проектами по обработке и анализу данных, что особенно полезно для обработки сложных шагов в крупномасштабных проектах. В MetaFlow проект организован в отдельные потоки, каждый из которых состоит из нескольких шагов, помеченных ключевым словом @step
.
Хорошо, идем!
Анализ данных с рентгенологическими отчетами
В этом конкретном разделе я объясню, почему важно знать свой набор данных. Глубокое понимание ваших данных похоже на наличие прочной базы для принятия разумных решений о том, как их организовать, особенно для обучения вашего компьютера классификации.
Отчеты о медицинской радиологии разделены на четыре основные категории: Результаты, Клинические данные, Название исследования и Впечатления. Результаты включают наблюдения, сделанные радиологами во время обследования, Клинические данные (показания) обозначают цель или причину проведения обследования, Название исследования указывает тип проведенного обследования, а Впечатления — это заметные клинические открытия. Ниже приведен пример, иллюстрирующий структуру типичного отчета:
Еще раз повторюсь, моя цель — научить компьютер точно определять соответствующий раздел, к которому принадлежит данное тело текста.
Теперь, когда вы лучше понимаете предысторию этого проекта, вот код!
Импорт данных
Первый шаг моего потока включает импорт набора данных и сбор данных из каждого столбца. Ключевое слово self.
используется для создания переменных, которые можно использовать на следующем шаге. Мы делаем это, чтобы отслеживать данные, которыми мы хотим делиться между шагами, поскольку каждый шаг в потоке выполняется независимо друг от друга.
from metaflow import FlowSpec, step class NLPFlow(FlowSpec): @step def start(self): import pandas as pd self.df = pd.read_csv("open_ave_data.csv") self.df.fillna("nan",inplace=True) self.find = self.df["findings"].values.tolist() self.clin = self.df["clinicaldata"].values.tolist() self.exam = self.df["ExamName"].values.tolist() self.impr = self.df["impression"].values.tolist() self.corpus = self.find + self.clin + self.exam + self.impr self.next(self.eda)
Исследовательский анализ данных
Отлично, теперь мы переходим к следующему этапу исследовательского анализа данных (EDA) с использованием функции self.next()
.
@step def eda(self): import matplotlib.pyplot as plt import numpy as np from functions import eda_len, eda_stop, eda_words, bigrams, wordcloud labels = ["impressions", "findings", "clinical data", "exam"] # character length eda_len(plt, labels, self.find, self.clin, self.exam, self.impr) # stop words for impressions eda_stop(plt, self.impr) # length of words eda_words(plt,labels, self.find, self.clin, self.exam, self.impr) # list of bigrams bigrams(plt, self.corpus) # wordcloud wordcloud(plt, self.corpus) self.next(self.preproc)
При использовании метапотока всегда рекомендуется делать код достаточно кратким. Вы заметите, что я импортирую множество функций из файла functions.py
, где я определил свой собственный набор функций для использования в сочетании с метапотоком. Итак, вот каждая функция, которая используется из этого файла, и соответствующий вывод графика для EDA.
Средняя длина символа
# function to calculate the mean length of each category def eda_len(plt, labels, find, clin, exam, impr): plt.clf() # impressions impr_lengths = np_length(impr) # findings find_lengths = np_length(find) # clinical data clin_lengths = np_length(clin) # exams exam_lengths = np_length(exam) char_len = [impr_lengths, find_lengths, clin_lengths, exam_lengths] x = range(len(char_len)) plt.bar(x, char_len) plt.xticks(x,labels) plt.savefig('text_length.png') # helper function to get the mean length of a list def np_length(lis): np_lis = np.array(lis) get_len = np.vectorize(len) str_len = get_len(lis) len_mean = np.mean(str_len) return len_mean
Среднее количество слов
def eda_words(plt, labels, findings, clinical, exam, impression): import nltk plt.clf() impr_counts = [len(nltk.word_tokenize(sentence)) for sentence in impression] impr_words = np.mean(impr_counts) find_counts = [len(nltk.word_tokenize(sentence)) for sentence in findings] findings_words = np.mean(find_counts) clin_counts = [len(nltk.word_tokenize(sentence)) for sentence in clinical] clin_words = np.mean(clin_counts) exam_counts = [len(nltk.word_tokenize(sentence)) for sentence in exam] exam_words = np.mean(exam_counts) words_bar = [impr_words, findings_words, clin_words, exam_words] x = range(len(words_bar)) plt.bar(x, words_bar) plt.xticks(x,labels) plt.savefig('words_length.png')
20 лучших стоп-слов
Стоп-слова — это термин, используемый в НЛП для обозначения определенных слов в языке, которые часто встречаются, но не имеют существенного значения. Когда дело доходит до создания языковых моделей, мы часто удаляем стоп-слова, чтобы сократить время обработки или повысить эффективность. Для анализа данных мы рассмотрим эти стоп-слова, но по мере продвижения мы будем удалять их во время предварительной обработки. Для следующего графика я нанес 20 лучших стоп-слов из категории показов.
# takes a body of text and finds the top 20 stop words def eda_stop(plt, lis): # nltk.download('punkt') # you will need this if you have not downloaded this already from nltk import FreqDist from nltk.corpus import stopwords from nltk.tokenize import word_tokenize plt.clf() stop=set(stopwords.words('english')) words = list(map(lambda x : word_tokenize(x),lis)) words = [word for i in words for word in i] stop_words = [i for i in words if i in stop] stop_freq = FreqDist(stop_words) top_20 = stop_freq.most_common(20) labels = [item[0] for item in top_20] values = [item[1] for item in top_20] plt.bar(labels, values, width=0.6) plt.savefig('impressions_stop_words.png')
N-грамм
В области НЛП N-граммы представляют последовательности из n элементов в тексте, которые могут быть как символами, так и словами. В этом случае я сосредоточусь на вычислении биграмм из всего корпуса (всех документов из набора данных), по сути определяя наиболее распространенные последовательности из двух слов в наборе данных.
def bigrams(plt, corpus): import pandas as pd from nltk import RegexpTokenizer from nltk.util import ngrams from nltk import FreqDist plt.clf() tokenizer = RegexpTokenizer(r'\b\w+\b') # Tokenize sentences into words tokenized_sentences = [tokenizer.tokenize(sentence) for sentence in corpus] # Create bigrams by words bigrams = [list(ngrams(sentence, 2)) for sentence in tokenized_sentences] bigrams = [bi for lis in bigrams for bi in lis] bi_freq = FreqDist(bigrams) # plotting the bigrams bi_fdist = pd.Series(dict(bi_freq.most_common(20))) bi_fdist.plot.bar() plt.xlabel('Bigram') plt.ylabel('Frequency') plt.title('Top 20 Bigrams') plt.tight_layout() # Ensures labels are not cut off plt.savefig('bigram_bar_chart.png') # Save the plot as an image
Вы заметите, что в приведенном выше коде я использую RegexpTokenizer, таким образом, я могу указать последовательность символов, которые я хочу считать токенами или словами. Таким образом, я буду избегать ненужных небуквенно-цифровых токенов, таких как точки, запятые, квадратные скобки или точки с запятой.
WordCloud
Последним сюжетом, который я вам покажу, будет сюжет WordCloud. Облака слов — это представления текстовых данных, в которых размер слов на изображении соответствует их важности или частоте в данных. Я буду использовать модуль wordcloud Python для его создания.
def wordcloud(plt, corpus): import wordcloud from wordcloud import WordCloud from nltk.corpus import stopwords text = ' '.join(corpus) wordcloud = WordCloud( width=1800, height=1400, max_words=100) wordcloud=wordcloud.generate(text) plt.figure(figsize=(10, 5)) plt.imshow(wordcloud, interpolation="bilinear") plt.axis("off") plt.savefig('word_cloud.png')
Выглядит отлично! Давайте перейдем к этапу предварительной обработки языковых моделей, которые мы будем использовать.
Предварительная обработка
Предварительная обработка — это шаги, которые необходимо предпринять для очистки и преобразования данных, чтобы их можно было использовать для анализа и языковых моделей. Наши шаги предварительной обработки будут включать удаление нежелательных символов, удаление стоп-слов, токенизацию (разделение предложений на отдельные слова или токены) и перевод в нижний регистр.
@step def preproc(self): from functions import target_vals, clean_data self.clean_corpus = list(map(clean_data,self.corpus)) self.corpus = [' '.join(words) for words in self.clean_corpus] # joining the words in a single string for ease of processing self.target_vals = target_vals(self.find, self.clin, self.exam, self.impr) self.colors = ['red', 'green', 'blue','yellow'] self.next(self.tfidf, self.word2vec) # next steps will be in parallel
Вот функции clean_data()
и target_vals()
из нашего файла functions.py
:
# function to clean data for processing def clean_data(w): import re from nltk.corpus import stopwords stopwords_list = stopwords.words('english') clean_corpus = [] w = w.lower() w=re.sub(r'[^\w\s]','',w) words = w.split() clean_words = [word for word in words if (word not in stopwords_list) and len(word) > 2] return clean_words # getting the target values for the model prediction def target_vals(find, clin, exam, impr): # repeating the label value for the length of the documents in the column f_y = [0] * len(find) c_y = [1] * len(clin) e_y = [2] * len(exam) i_y = [3] * len(impr) # combine all the labels category_labels = f_y + c_y + e_y + i_y return category_labels
Вы могли заметить, что я создал функцию под названием target_vals()
. При создании моделей классификации важно обучать их с помощью размеченных данных. В машинном обучении эта концепция называется Обучение с учителем. Данные помечаются определенными категориями, и модель учится различать закономерности в данных и делать соответствующие прогнозы. Чтобы оценить точность этих прогнозов, мы используем целевые метки, которые в этом примере представляют правильные категории для всех документов, на которых будет тестироваться модель.
Следующий шаг — или, скорее, следующие несколько шагов — будет выполняться параллельно. Использование мощной функциональности ветвей MetaFlow. Мы достигаем этого, используя несколько параметров в нашей функции self.next()
в конце нашего шага. Наш модуль метапотока будет распараллеливать шаги для нас, и нам не придется беспокоиться о том, как мы будем использовать ядра на нашем компьютере.
Частота термина — обратная частота документа (TD-IDF)
Частота терминов — обратная частота документов — это процесс количественной оценки важности слов в наборе документов. Частота термина вычисляет частоту слова в документе. Обратная частота документа вычисляет важность слова в коллекции всех документов. Оба этих значения создают разреженное векторное представление для документа. Это числовое представление можно использовать для сравнения или классификации.
Вот следующие шаги для TF-IDF:
@step def tfidf(self): from sklearn.feature_extraction.text import TfidfVectorizer vectorizer = TfidfVectorizer() self.tfidf_documents = vectorizer.fit_transform(self.corpus) self.next(self.tfidf_plot) @step def tfidf_plot(self): from sklearn.decomposition import PCA import matplotlib.pyplot as plt from functions import tfidf_plot labels = ['Findings', 'Clinical Data', 'Exam Name', 'Impressions'] tfidf_plot(self.tfidf_documents, plt, self.colors, labels) self.tfidf_documents self.next(self.join)
Поскольку TF-IDF являются векторами, эти значения можно представить в виде графика.
def tfidf_plot(tfidf, plt, colors, labels): print("tfidf plot") from sklearn.decomposition import PCA pca = PCA(n_components=2) tfidf_matrix_2d = pca.fit_transform(tfidf.toarray()) color = -1 label = -1 for i, document in enumerate(tfidf_matrix_2d): if i % 954 == 0: color+=1 label+=1 x_coords = document[0] y_coords = document[1] plt.scatter(x_coords, y_coords, color=colors[color], label=labels[label]) # Set plot title and axis labels. plt.title("TF-IDF Matrix Scatter Plot") plt.xlabel("Dimension 1") plt.ylabel("Dimension 2") # Display the scatter plot. plt.savefig("tfidf.png")
Как вы можете видеть на графике выше, каждая точка представляет собой документ, а каждый цвет соответствует категории. Вы можете увидеть, как похожие документы обычно группируются вместе. Это также помогает количественно определить границы категории, что может быть полезно при классификации.
В следующих параллельных шагах мы изучим Word2Vec, еще одну форму векторного представления.
Word2Vec
Word2Vec — это метод изучения встраивания слов. Он представляет слова как плотные векторы в непрерывном пространстве. В этом проекте мы будем использовать Word2Vec для представления слов в документе. Вот следующий шаг потока:
@step def word2vec(self): import multiprocessing from gensim.models import Word2Vec import numpy as np from functions import word2vec_avg self.corpus = self.clean_corpus cores = multiprocessing.cpu_count() self.w2v_model = Word2Vec(min_count=5,window=5,vector_size=300,workers=cores-1,max_vocab_size=100000) self.w2v_model.build_vocab(self.corpus) self.w2v_model.train(self.corpus,total_examples=self.w2v_model.corpus_count,epochs=50) self.document_vectors = np.array(word2vec_avg(self.find, self.clin, self.exam, self.impr, self.corpus, self.w2v_model)) self.next(self.word2vec_plot)
Единственная загвоздка здесь в том, что каждый вектор Word2Vec представляет только одно слово, но для нашего анализа нам нужно, чтобы он представлял весь документ. Поэтому нам нужно усреднить все векторы Word2Vec из каждого слова в каждом документе. Тогда усредненный вектор будет представлением этого документа. Вот код для этого:
# averages all the word2vec vectors for each document def word2vec_avg(find, clin, exam, impr, corpus, model): f = len(find) c = len(clin) e = len(exam) i = len(impr) document_vectors = [] for doc in corpus: vectors = [model.wv[word] for word in doc if word in model.wv] if vectors: doc_vector = np.mean(vectors, axis=0) else: doc_vector = np.zeros(300) document_vectors.append(doc_vector) return document_vectors
Затем давайте нанесем эти векторы на график, чтобы понять, как они выглядят:
@step def word2vec_plot(self): from sklearn.manifold import TSNE import matplotlib.pyplot as plt from functions import word2vec_plot tsne = TSNE(n_components=2, random_state=42) vectors_2d = tsne.fit_transform(self.document_vectors) word2vec_plot(plt, vectors_2d, self.colors) self.document_vectors self.target_vals self.next(self.join) def word2vec_plot(plt, vectors_2d, colors): print("word2vec plot") color = -1 plt.figure(figsize=(10, 8)) for i, word in enumerate(vectors_2d): if i % 954 == 0: color+=1 x, y = vectors_2d[i, :] plt.scatter(x, y, c=colors[color]) plt.savefig("word2vec.png")
График выглядит немного разреженным и сильно отличается от TF-IDF, но помните, что эти значения усреднены, что может повлиять на разницу во внешнем виде графика.
Нашими следующими шагами будет тестирование наших моделей TF-IDF и Word2Vec, чтобы определить, какая из них лучше предсказывает категории.
Логистическая регрессия
Логистическая регрессия — это алгоритм статистического и машинного обучения, используемый для задач бинарной классификации, цель которых — предсказать вероятность принадлежности выборки данных к определенному классу. Хотя этот проект содержит несколько классов или категорий. Это просто означает, что модели придется адаптировать мультиклассификацию к бинарному сценарию. Модель будет разделена на несколько моделей, по одной для каждой категории, где положительный прогноз — это тот, который соответствует правильной категории, а тот, который соответствует другой, неправильной категории, будет считаться отрицательным прогнозом. Здесь нам нужно будет использовать target_vals
, который мы определили ранее.
Вот как создать модель логистической регрессии:
def logistic_train(x,target): # target vals is input for this function from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score X = np.array(x) y = np.array(target) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=24) log_reg = LogisticRegression() log_reg.fit(X_train, y_train) y_pred = log_reg.predict(X_test) # Calculate accuracy of the model return accuracy_score(y_test, y_pred)
Метрики
После обучения и тестирования модели рассчитаем точность работы модели. Точность — это один из многих показателей, которые мы можем использовать для определения того, насколько «хорошо» работает модель. Он рассчитывается путем оценки количества точных прогнозов по отношению к общему количеству тестовых образцов. Хотя использование точности может быть ценной метрикой, другие меры, такие как точность, полнота или F-оценка, также полезны для оценки производительности. Несмотря на свои ограничения, во многих случаях точность все еще может быть практичной и информативной метрикой.
Вот шаги для обучения моделей Word2Vec и TF-IDF:
# word2vec training step @step def word2vec_train(self): import numpy as np from functions import logistic_train target_vals = self.target_vals # calculate w2v accuracy self.w2v_acc = logistic_train(self.document_vectors, target_vals) self.next(self.join) # tfidf training step @step def tfidf_train(self): import numpy as np from functions import logistic_train # calculate tfidf accuracy tfidf_arr = self.tfidf_documents.toarray() self.tfidf_acc = logistic_train(tfidf_arr, self.target_vals) self.next(self.join)
Следующий шаг в потоке объединяет два параллельных процесса и сравнивает точность двух моделей.
# compare the models in join step @step def join(self, inputs): import numpy as np from functions import logistic_train # calculate w2v accuracy print("W2V Accuracy:", inputs.word2vec_train.w2v_acc) print("TFIDF Accuracy:", inputs.tfidf_train.tfidf_acc) self.next(self.end)
Затем мы увидим вывод терминала о выполнении этого потока и решим, что лучше для этой задачи — TF-IDF или Word2Vec. Я запущу поток, введя команду python <file.py> run
:
Здесь мы видим, что каждый шаг завершился успешно, и наша модель Word2Vec имеет точность 99,6%, а TF-IDF — 99,7%.
Стоит отметить, что этот набор данных относительно небольшой и содержит всего 954 отчета. В результате при многократном запуске и тестировании моделей разница в точности между ними была минимальной. Этот пример стал первым случаем, когда модели демонстрировали значительные различия в значениях для случайного начального числа при создании разделения обучения и тестирования.
Краткое содержание
В общем, результаты немного предсказуемы. Word2Vec — это модель, которая представляет каждое словов виде вектора в непрерывном пространстве. Он идеально подходит для ситуаций, когда важна семантика, потому что он может помочь определить сходство между словами, но это означает, что он рассматривает слово не в контексте документа, а само по себе или его соседство с другими словами. Отсюда, почему его общая производительность была ниже — хотя и ненамного.
В этом конкретном случае TF-IDF имеет преимущество из-за учета контекста слова в документе, а не отдельных вхождений. Этот подход включает как частоту слова в документе, так и его частоту во всех других документах, чтобы сформировать разреженный вектор.
Например, термин "ВПЕЧАТЛЕНИЕ" может появляться только несколько раз в любом документе категории показов. Таким образом, хотя частота терминов в документах может быть низкой, по сравнению с другими документами, это слово с большей вероятностью будет появляться в категории показов, что подчеркивает его важность с точки зрения обратной частоты документов. Таким образом, повышается точность классификации.
С более широкой точки зрения, определение наиболее подходящей модели зависит от конкретной рассматриваемой проблемы. Каждый подход имеет свои сильные стороны и ограничения. Очень важно тщательно взвесить эти факторы и сопоставить их с целями проблемы, которую вы решаете. Это тщательное понимание гарантирует, что выбранная модель хорошо работает с конкретными трудностями и деталями вашего проекта.
Вот мой полный код по моей ссылке Проект GitHub.
Спасибо за чтение!
Лорен Уильямс