Помогите мировому сообществу лучше понять болезнь, проведя огромное количество исследований.

Проект

В ответ на пандемию COVID-19 Белый дом и коалиция ведущих исследовательских групп подготовили набор данных открытых исследований COVID-19.

Это сборник большого количества исследовательских работ о COVID-19. Мы надеемся, что сообщество машинного обучения поможет извлечь полезные идеи из этой большой коллекции исследовательских работ.

Данные и подробное описание можно найти здесь на Kaggle.

Что такое идея?

Идея состоит в том, чтобы создать языковую модель, обученную на «основном тексте» исследовательской работы, доступной в этом наборе данных. Полученную языковую модель можно затем использовать для создания своего рода классификатора.

В настоящее время у нас нет помеченного набора данных о COVID-19, но я надеюсь, что в случае доступности помеченного набора данных мы сможем использовать эту языковую модель для выполнения какой-либо задачи классификации.

Даже если помеченный набор данных недоступен, мы все равно можем настроить эту языковую модель (или ее улучшенную версию), чтобы предсказать следующую последовательность слов для поискового запроса и получить скрытую информацию, которая в противном случае была бы скрыта от человеческий глаз.

Импорт необходимых библиотек

Я использую fastai для создания языковой модели. Итак, импортируется fast.text. Json используется для анализа данных json.

from fastai.text import *
import numpy as np
import json

Давайте установим все пути?

Давайте установим весь путь, необходимый для работы с нашими данными.

  • path — это корневой входной каталог.
  • outputPath — это то место, где мы будем сохранять наш csv, модели и т. д.
  • datasetPath — это каталог для набора данных.
path = Path('/kaggle/input')
outputPath = Path('/kaggle/working')
datasetPath = path/'CORD-19-research-challenge/'

Давайте также установим путь к коллекции научных статей. Для этой языковой модели мы выберем подмножество всего корпуса. Это поможет нам в быстром первоначальном тестировании нашего кода.

  • pmcCustomLicense — это путь к каталогу, содержащему файлы json для исследовательских работ, полученных из pmc.
  • biorxivMedrxiv — это путь к каталогу, содержащему json-файлы исследовательских работ, взятых из biorxiv-Medrxiv.
pmcCustomLicense = datasetPath/'custom_license/custom_license'
biorxivMedrxiv = datasetPath/'biorxiv_medrxiv/biorxiv_medrxiv'

Как разобрать данные из Json?

Данные из исследовательских работ хранятся в этом наборе данных в формате json. Нам нужно проанализировать данные из файлов json, прежде чем использовать это.

Для этого проекта я пытаюсь построить языковую модель, и в настоящее время меня интересует ключ «body_text» файлов json.

Это где корпус научных работ присутствует.

Здесь я использовал функции и приемы, уже использованные в этом ноутбуке. Я благодарен создателю этого блокнота за это.

Я внес несколько изменений в функции оригинальной записной книжки, но почти все они остались такими же, как исходное содержимое.

Здесь идея состоит в том, чтобы проанализировать файлы json для извлечения основного текста в кадр данных pandas.

Функции делают следующее →

load_files() -

  • Принимает кортеж в качестве аргумента. Этот кортеж представляет собой набор списков, имеющих путь к каталогам, из которых необходимо получить json.
  • Затем эта функция проходит через каждый из этих каталогов и добавляет их в новый список fileList. таким образом содержимое исходных списков (т.е. filpaths) становится частью большего списка fileList.

пример использования

filePathList1 = [‘path1’, ‘path2’, ‘path3’] filePathList2 = [‘path4’, ‘path5’, ‘path6’]
load_files(filePathList1, filePathList2)
Output → [‘path1’, ‘path2’, ‘path3’, ‘path4’, ‘path5’, ‘path6’]
def load_files(fileNames: tuple):
    fileList = []
for file in fileNames:
        fileList.append(file)
        
    return fileList

Затем мы вызываем функцию с предпочтительным каталогом, в котором хранятся исследовательские работы.

files = load_files((biorxivMedrxiv.iterdir()))

Вы можете заметить, что я не передал biorxivMedrxiv как есть. Причина в том, что biorxivMedrxiv — это объект «posixpath», а не итерируемый объект. Чтобы перебрать biorxivMedrxiv, я использовал метод iterdir(), который дает нам итератор.

Пути к файлам, обработанные с помощью load_files(), содержат объекты пути и могут не подходить для определенных операций.

Например, объекты пути нельзя повторять, так как содержимое, возвращаемое load_files(), является объектами «posixpath».

Мы преобразуем эти объекты пути в их строковые «пути» с помощью функции filePath(). Эта функция делает следующее →

  • Принимает в списке объекты пути.
  • Перебирает список и преобразует каждый элемент списка, т. е. объекты пути, в строковый формат.
  • Затем он помещает эти строковые пути в список файловых путей.

Примечание. Для функции load_files() я передал в кортеже списка файлов значение iterdir(), чтобы мы могли перебирать эти пути. Итак, я думаю, что содержимое, возвращаемое функцией load_files(), можно повторять без использования функции filePath(). Однако я еще не проверял это.

def filePath(files):
    filePath = []
    
    for file in files:
        filePath.append(str(file))
        
    return filePath

Затем мы вызываем функцию.

filePaths=filePath(files)

getRawFiles() функция --›

  • Принимает список путей к файлам
  • Проходит по каждому пути к файлу из списка files и использует метод json.load() для чтения содержимого.
  • наконец, он добавляет этот объект json в список rawFiles.
def getRawFiles(files: list):
    rawFiles = []
        
    for fileName in files:
            rawFile = json.load(open(fileName, 'rb'))
            rawFiles.append(rawFile)
            
    return rawFiles

Вызов этой функции даст нам необработанное содержимое файлов json.

rawFiles = getRawFiles(filePaths)

format_name()

  • Принимает объект json с автором в качестве ключа.
  • Соединяет имена в следующей последовательности ‹имя› ‹отчество› ‹фамилия.
  • Функция также позаботится о том, что если отчества нет, то просто введите ‹имя› ‹фамилия

format_affiliation()

  • Принимает объект json с ключом в качестве принадлежности.
  • Если сведения о местоположении есть в json, поместите их в список и верните.
  • Если учреждение есть, соедините данные о местонахождении и учреждении и поместите их в список.

format_authors()

  • Принимает объект json с ключом в качестве автора.
  • Соединяет имя автора с аффилиациями, если аффилиации доступны.

format_body()

  • Принимает объект json с ключом как body_text.
  • Извлекает текст, а затем добавляет его в список.

format_bib()

  • Принимает объект json с ключом в качестве нагрудника.
  • Объединяет «название», «авторы», «место проведения», «год» вместе, чтобы сформировать строку.
def format_name(author):
    middle_name = " ".join(author['middle'])
    
    if author['middle']:
        return " ".join([author['first'], middle_name, author['last']])
    else:
        return " ".join([author['first'], author['last']])
def format_affiliation(affiliation):
    text = []
    location = affiliation.get('location')
    if location:
        text.extend(list(affiliation['location'].values()))
    
    institution = affiliation.get('institution')
    if institution:
        text = [institution] + text
    return ", ".join(text)
def format_authors(authors, with_affiliation=False):
    name_ls = []
    
    for author in authors:
        name = format_name(author)
        if with_affiliation:
            affiliation = format_affiliation(author['affiliation'])
            if affiliation:
                name_ls.append(f"{name} ({affiliation})")
            else:
                name_ls.append(name)
        else:
            name_ls.append(name)
    
    return ", ".join(name_ls)
def format_body(body_text):
    texts = [(di['section'], di['text']) for di in body_text]
    texts_di = {di['section']: "" for di in body_text}
    
    for section, text in texts:
        texts_di[section] += text
body = ""
for section, text in texts_di.items():
        body += section
        body += "\n\n"
        body += text
        body += "\n\n"
    
    return body
def format_bib(bibs):
    if type(bibs) == dict:
        bibs = list(bibs.values())
    bibs = deepcopy(bibs)
    formatted = []
    
    for bib in bibs:
        bib['authors'] = format_authors(
            bib['authors'], 
            with_affiliation=False
        )
        formatted_ls = [str(bib[k]) for k in ['title', 'authors', 'venue', 'year']]
        formatted.append(", ".join(formatted_ls))
return "; ".join(formatted)

generate_clean_df() -->

  • Использует вышеуказанные вспомогательные функции для создания кадра данных pandas.
  • Результирующий фрейм данных будет иметь следующие столбцы: «paper_id», «название», «авторы», «принадлежности», «аннотация», «текст», «библиография», «raw_authors», «raw_bibliography».
def generate_clean_df(all_files):
    cleaned_files = []
    
    for file in all_files:
        features = [
            file['paper_id'],
            file['metadata']['title'],
            format_authors(file['metadata']['authors']),
            format_authors(file['metadata']['authors'], 
                           with_affiliation=True),
            format_body(file['abstract']),
            format_body(file['body_text']),
            format_bib(file['bib_entries']),
            file['metadata']['authors'],
            file['bib_entries']
        ]
cleaned_files.append(features)
col_names = ['paper_id', 'title', 'authors',
                 'affiliations', 'abstract', 'text', 
                 'bibliography','raw_authors','raw_bibliography']
clean_df = pd.DataFrame(cleaned_files, columns=col_names)
    clean_df.head()
    
    return clean_df

Затем вызов этой функции сгенерирует наш фрейм данных.

pd.set_option('display.max_columns', None) 
cleanDf = generate_clean_df(rawFiles)
cleanDf.head()

Наконец, сохраните фрейм данных в файл csv. Поскольку весь предыдущий процесс занимает немного времени, поэтому сохранение файла в csv сэкономит время позже.

cleanDf.to_csv(outputPath/'cleandf.csv')

Данные

Затем мы создаем пакет данных из csv, который мы создали из файлов json.

createDataBunchForLanguageModel() — это вспомогательная функция. Это делает следующее →

  • Создает пакет данных из CSV-файла.
  • Данные в пакете данных извлекаются из столбца «текст» CSV-файла.
  • 10% данных зарезервированы для проверки.
  • Поскольку языковая модель, которую мы создадим в дальнейшем, будет моделью с самоконтролем. Он получает метку из самих данных.
  • Таким образом, label_for_lm() помогает нам в этом.
  • После создания пакета данных он сохраняется в файле pickle.
  • В следующий раз, когда этот код запустится, нам не нужно снова проходить процесс создания пакета данных. Мы можем использовать файл pickle.

Чтобы узнать больше о databunch, обратитесь к моей предыдущей статье здесь.

def createDataBunchForLanguageModel(outputPath: Path,
                                    csvName: str,
                                    textCol: str, 
                                    pickelFileName: str,
                                    splitBy: float, 
                                   batchSize: int):
    data_lm = TextList.from_csv(outputPath,
                                f'{csvName}.csv',
                                cols=textCol)\
                  .split_by_rand_pct(splitBy)\
                  .label_for_lm()\
                  .databunch(bs=batchSize)
    
    data_lm.save(f'{pickelFileName}.pkl')

Вызов этой функции для создания пакета данных.

createDataBunchForLanguageModel(outputPath,
                                    'cleandf',
                                    'text', 
                                    'cleanDf',
                                    0.1,
                                    48)

loadData() — это вспомогательная функция для загрузки файла пакета данных. Он принимает требуемый размер пакета, который мы хотим загрузить из пакета данных.

def loadData(outputPath: Path,
             databunchFileName: str,
             batchSize: int,
             showBatch: bool= False):
    
    data_lm = load_data(outputPath,
                       f'{databunchFileName}.pkl',
                       bs=batchSize)
    
    if showBatch:
        data_lm.show_batch()
        
    return data_lm

Мы будем использовать эту функцию в ближайшее время.

Развитие ученика

Мы используем ULMFIT для создания языковой модели в корпусе исследовательских данных, а затем тонко настраиваем эту языковую модель.

Мы создаем языковую модель учащегося. Language_model_learner() — это быстрый метод, который помогает нам создать объект учащегося с данными, созданными в предыдущих разделах.

Здесь мы используем размер пакета 48. Этот учащийся построен с использованием архитектуры предварительно обученной языковой модели AWD_LSTM. Это самоконтролируемая часть. Это модель, которая была обучена на корпусе английского языка для предсказания следующей последовательности предложений и, таким образом, понимает структуру языка.

Нам просто нужно точно настроить эту модель в нашем корпусе данных, который затем можно использовать для создания классификатора.

Я еще не строю классификатор, потому что понятия не имею, что нам нужно классифицировать. Тем не менее, я считаю, что эта точно настроенная языковая модель может быть адаптирована для поиска скрытой информации в большом корпусе исследовательских работ, которые в противном случае были бы скрыты/пропущены читателями-людьми.

Примечание.Я не буду углубляться в ULMFIT, так как это не является целью этого поста. Возможно позже сделаю подробный пост. Однако подробное объяснение ULMFIT можно найти здесь на fastai.

learner = language_model_learner(loadData(outputPath,
             'cleanDf',
             48,
             showBatch= False),
             AWD_LSTM,
             drop_mult=0.3)

Мы наносим скорость обучения для нашей языковой модели. На этом графике мы попытаемся найти наилучшую скорость обучения, подходящую для нашей модели. plotLearningRate() — это вспомогательная функция, которая отображает для нас скорость обучения.

Объяснение того, как использовать скорость обучения, можно найти здесь.

def plotLearningRate(learner, skip_end):
    learner.lr_find()
    learner.recorder.plot(skip_end=skip_end)

Теперь мы наносим скорость обучения.

plotLearningRate(learner, 15)

Затем мы принимаем эту скорость обучения в качестве отправной точки, где сюжет расходится. Затем модель обучается с помощью политики «подгонка одного цикла» с этой начальной скоростью обучения.

Здесь мы тренируем только голову.

learner.fit_one_cycle(1, 1e-02, moms=(0.8,0.7))

Поскольку мы создаем языковую модель, мы не слишком беспокоимся о том, чтобы получить здесь максимально возможную точность. Размораживаем сеть и обучаем еще.

learner.unfreeze()
learner.fit_one_cycle(10,1e-3, moms=(0.8,0.7))

Наконец, мы сохраняем точно настроенную языковую модель для последующего использования в тестировании/прогнозировании.

learner.save('fineTuned')

Тестирование

Давайте посмотрим, сможет ли модель подключить информацию/знания из корпуса. Сначала мы загружаем сохраненную модель, а затем пытаемся найти прогноз для поискового запроса.

learner = learner.load('fineTuned')

Давайте возьмем тестовое предложение в качестве входных данных.

TEXT = "Range of incubation periods for the disease in humans"
N_WORDS = 40
N_SENTENCES = 2

Мы увидим, сможет ли наша модель предсказать слова, связанные с нашей тестовой строкой.

print("\n".join(learner.predict(TEXT, N_WORDS, temperature=0.75) for _ in range(N_SENTENCES)))

Сейчас у нас есть довольно хорошая языковая модель. Теперь, когда мы точно настроили нашу языковую модель, мы можем сохранить модель, предсказывающую следующую последовательность слов, и кодировщик, отвечающий за создание и обновление скрытых состояний.

Сохраненную модель можно использовать для переноса обучения в дальнейших задачах, а сохраненный кодировщик можно использовать для создания классификатора для текстовой классификации любого помеченного набора данных, связанного с COVID-19.

Мы будем использовать .save() для сохранения файла .pth.

Упомянутая ниже функция save() является вспомогательной функцией, которая может сохранять только модель или кодировщик и модель в отдельных файлах.

def save(learner,
         saveEncoder: bool = True):
    
    if saveEncoder:
        learner.save_encoder('fine_tuned_encoder')
        
    learner.save('fine_tuned_model')

вызов этой функции сделает экономию за вас.

save(learner)

Конечные примечания

Я надеюсь, что это ядро ​​и эта языковая модель будут полезны для кого-то, кто может работать с этим набором данных или любыми данными, связанными с COVID-19.

Я надеюсь, что кто-то, кто более опытен в НЛП и глубоком обучении, улучшит эту модель и сделает что-то полезное, что затем может способствовать борьбе с COVID-19.

Я никоим образом не являюсь экспертом в НЛП, поэтому сеть, которую я разработал здесь, основана на коде из урока 3 курса — Практика глубокого обучения для кодеров от fastai».

Я надеюсь, что кто-то, кто более опытен в НЛП и глубоком обучении, улучшит эту модель и выведет что-то полезное, что затем может помочь нам в борьбе с COVID-19.

Код, использованный в этом посте, доступен в виде ядра kaggle здесь и был отправлен в рамках одной из задач задачи Данные исследования COVID-19.