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

В этой статье я покажу, как построить свёрточную нейронную сеть, способную различать типы рака, используя простой классификатор PyTorch. Данные и код, использованные для обучения, находятся в открытом доступе, и обучение можно провести на персональном компьютере, возможно, даже на центральном процессоре.
Рак — это досадный побочный эффект накопления информационных ошибок в клетках на протяжении жизни, что приводит к неконтролируемому росту. В качестве исследователей мы изучаем закономерности этих ошибок, чтобы лучше понять это заболевание. С точки зрения специалиста по данным, геном человека представляет собой строку длиной около трёх миллиардов букв, состоящую из букв A, C, G и T (то есть 2 бита информации на букву). Ошибка копирования или внешнее событие могут потенциально удалить/вставить/изменить букву, вызывая мутацию и потенциальное нарушение функции генома.
Однако отдельные ошибки практически никогда не приводят к развитию рака. В организме человека существует множество механизмов, предотвращающих развитие рака, включая специальные белки — так называемые опухолевые супрессоры. Для того чтобы клетка могла обеспечить устойчивый рост, необходимо выполнение ряда условий — так называемых «признаков рака».

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

Эти обширные изменения структуры генома приводят к потере генов, препятствующих развитию рака, и накоплению генов, способствующих росту клеток. Секвенирование ДНК этих клеток позволяет выявить эти изменения, что довольно часто происходит в областях, специфичных для данного типа рака. Значения числа копий для каждого аллеля можно получить из данных секвенирования с помощью генераторов числа копий.
Обработка профилей номеров копий
Одно из преимуществ работы с профилями числа копий (CN) заключается в том, что они не являются биометрическими и, следовательно, могут быть опубликованы без ограничений доступа. Это позволяет нам накапливать данные из различных исследований с течением времени для создания наборов данных достаточного размера. Однако данные, полученные в ходе разных исследований, не всегда можно напрямую сравнивать, поскольку они могут быть получены с использованием разных технологий, иметь разное разрешение или быть предварительно обработаны разными способами.
Для получения данных, их совместной обработки и визуализации мы будем использовать инструмент CNSistent, разработанный в рамках работы Института вычислительной биологии рака Университетской клиники г. Кельн, Германия.
Сначала мы клонируем репозиторий и данные и устанавливаем версию, используемую в этом тексте:
git clone [email protected]:schwarzlab/cnsistent.git cd cnsistent git checkout v0.9.0
Поскольку данные, которые мы будем использовать, находятся внутри репозитория (около 1 ГБ), загрузка займёт несколько минут. Для клонирования в системе должны быть установлены как Git, так и Git LFS.
Внутри репозитория находится файл requirements.txt, в котором перечислены все зависимости, которые можно установить с помощью pip install -r requirements.txt.
(Рекомендуется сначала создать виртуальную среду.) После установки всех необходимых компонентов CNSistent можно установить, выполнив команду pip install -e . в той же папке. Флаг -e устанавливает пакет из исходного каталога, что необходимо для доступа к данным через API.
Репозиторий содержит необработанные данные из трёх наборов данных: TCGA, PCAWG и TRACERx. Их необходимо предварительно обработать. Это можно сделать, запустив скрипт bash ./scripts/data_process.sh.
Теперь мы обработали наборы данных и можем загрузить их с помощью библиотеки утилит данных CNSistent:
импортируйте cns.data_utils как cdu samples_df, cns_df = cdu.main_load(«imp») print(cns_df.head())
Получаем следующий результат:
| | sample_id | chrom | начало | конец | major_cn | minor_cn | |—:|:————|:———|———:|———:|————:|————:| | 0 | SP101724 | chr1 | 0 | 27256755 | 2 | 2 | | 1 | SP101724 | chr1 | 27256755 | 28028200 | 3 | 2 | | 2 | SP101724 | chr1 | 28028200 | 32976095 | 2 | 2 | | 3 | SP101724 | chr1 | 32976095 | 33354394 | 5 | 2 | | 4 | SP101724 | chr1 | 33354394 | 33554783 | 3 | 2 |
В этой таблице приведены данные о количестве копий со следующими столбцами:
- sample_id: идентификатор образца,
- хром: хромосома,
- начало: начальная позиция сегмента (с индексом 0 включительно),
- конец: конечная позиция сегмента (исключая 0-индексную),
- major_cn: количество копий основного аллеля (большего из двух),
- minor_cn: количество копий минорного аллеля (меньший из двух).
Таким образом, на первой строке мы видим сегмент, указывающий на то, что образец SP101724 имеет 2 копии основного аллеля и 2 копии минорного аллеля (всего 4) в области хромосомы 1 от 0 до 27,26 мегабаз.
Второй загруженный нами фрейм данных, samples_df, содержит метаданные образцов. Для наших целей важен только тип. Мы можем просмотреть доступные типы, выполнив:
импортируйте matplotlib.pyplot как plt type_counts = sampling_df[«type»].value_counts() plt.figure(figsize=(10, 6)) type_counts.plot(kind='bar') plt.ylabel('Count') plt.xticks(rotation=90)

В приведённом выше примере мы видим потенциальную проблему с данными: длина отдельных сегментов неравномерна. Первый сегмент имеет длину 27,26 мегабаз, а второй — всего 0,77 мегабаз. Это проблема для нейронной сети, которая ожидает, что входные данные будут фиксированного размера.
Технически мы могли бы взять все существующие точки останова и создать сегменты между ними в наборе данных, так называемая минимально согласованная сегментация. Однако это привело бы к огромному количеству сегментов — быстрая проверка с помощью len(cns_df[“end”].unique()) показывает, что существует 823652 уникальные точки останова.
В качестве альтернативы, мы можем использовать CNSistent для создания новой сегментации с помощью алгоритма биннинга. Это создаст сегменты фиксированного размера, которые можно использовать в качестве входных данных для нейронной сети. В нашей работе мы определили сегменты размером 1–3 мегабазы, обеспечивающие наилучший компромисс между точностью и переобучением. Сначала мы создаём сегментацию, а затем применяем её для получения новых CNS-файлов с помощью следующего Bash-скрипта:
threads=8 cns segment whole —out «./out/segs_3MB.bed» —split 3000000 —remove gaps — filter 300000 для набора данных в TRACERx PCAWG TCGA_hg19; do cns aggregate ./out/${dataset}_cns_imp.tsv — segments ./out/segs_3MB.bed — out ./out/${dataset}_bin_3MB.tsv — samples ./out/${dataset}_samples.tsv — threads $threads done
Цикл обрабатывает каждый набор данных отдельно, сохраняя при этом ту же сегментацию. Флаг —threads используется для ускорения процесса за счёт параллельного выполнения агрегации, корректируя значение в зависимости от количества доступных ядер.
Аргументы —remove gaps —filter 300000 удаляют области с низкой отображаемостью (также известные как пробелы) и отфильтровывают сегменты короче 300 КБ. Аргумент —split 3000000 создаёт сегменты размером 3 МБ.
Немелкоклеточный рак легкого
В данной статье мы сосредоточимся на классификации немелкоклеточного рака лёгкого, на долю которого приходится около 85% всех случаев рака лёгкого, в частности на различии между аденокарциномой и плоскоклеточным раком. Важно различать эти два вида рака, поскольку схемы их лечения будут различаться, а новые методы дают надежду на неинвазивную диагностику с помощью образцов крови или мазков из носа.
Мы используем полученные выше сегменты и загрузим их с помощью предоставленной функции полезности. Поскольку мы классифицируем рак по двум типам, мы можем отфильтровать выборки, включив только соответствующие типы: LUAD (аденокарцинома) и LUSC (плоскоклеточный рак), и построить график для первой выборки:
импорт cns samples_df, cns_df = cdu.main_load(«3MB») samples_df = sample_df.query(«введите ['LUAD', 'LUSC']») cns_df = cns.select_CNS_samples(cns_df, samples_df) cns_df = cns.only_aut(cns_df) cns.fig_lines(cns.cns_head(cns_df, n=3))
Сегменты числа основных и второстепенных копий в бинах по 3 МБ для первых трёх образцов. В данном случае все три образца получены в результате многорегионального секвенирования одного и того же пациента, что демонстрирует, насколько гетерогенными могут быть раковые клетки даже в пределах одной опухоли.
Модель сверточной нейронной сети
Для запуска кода требуется установленный Python 3 с PyTorch 2+ и оболочка, совместимая с Bash. Для более быстрого обучения рекомендуется использовать графический процессор NVIDIA, но это не обязательно.
Сначала мы определим сверточную нейронную сеть с тремя слоями:
импортируйте torch.nn как nn класс CNSConvNet(nn.Module): def __init__(self, num_classes): super(CNSConvNet, self).__init__() self.conv_layers = nn.Sequential( nn.Conv1d(входящие_каналы=2, выходные_каналы=16, размер_ядра=3, заполнение=1), nn.ReLU(), nn.MaxPool1d(размер_ядра=2), nn.Conv1d(входящие_каналы=16, выходные_каналы=32, размер_ядра=3, заполнение=1), nn.ReLU(), nn.MaxPool1d(размер_ядра=2), nn.Conv1d(входящие_каналы=32, выходные_каналы=64, размер_ядра=3, заполнение=1), nn.ReLU(), nn.MaxPool1d(kernel_size=2) ) self.fc_layers = nn.Sequential( nn.LazyLinear(128), nn.ReLU(), nn.Dropout(0.5), nn.Linear(128, num_classes) ) def forward(self, x): x = self.conv_layers(x) x = x.view(x.size(0), -1) x = self.fc_layers(x) return x
Это шаблонная глубокая сверточная нейронная сеть с двумя входными каналами — по одному на каждый аллель — и тремя свёрточными слоями с одномерным ядром размера 3 и функцией активации ReLU. За свёрточными слоями следуют слои максимального пулинга с размером ядра 2. Свёртка традиционно используется для обнаружения краёв, что полезно для нас, поскольку нас интересуют изменения числа копий, то есть краёв сегментов.
Выходные данные свёрточных слоёв затем сглаживаются и пропускаются через два полносвязных слоя с дропаутом. LazyLinearlayer объединяет выходные данные 64 каналов в один слой из 128 узлов, без необходимости вычисления количества узлов в конце свёртки. Именно там находится большинство наших параметров, поэтому мы также применяем дропаут для предотвращения переобучения.
Обучение модели
Сначала нам нужно преобразовать данные из фреймов данных в тензоры Torch. Мы используем вспомогательную функцию bins_to_features, которая создаёт трёхмерный массив признаков нужного формата (выборки, аллели, сегменты). В процессе мы также разбиваем данные на обучающий и тестовый наборы в соотношении 4:1:
import torch from torch.utils.data import TensorDataset, DataLoader from sklearn.preprocessing import LabelEncoder from sklearn.model_selection import train_test_split # конвертация данных в признаки и метки features, samples_list, columns_df = cns.bins_to_features(cns_df) # конвертация данных в тензоры Torch X = torch.FloatTensor(features) label_encoder = LabelEncoder() y = torch.LongTensor(label_encoder.fit_transform(samples_df.loc[samples_list][«type»])) # Разделение тест/обучение X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0) # Создание загрузчиков данных train_loader = DataLoader(TensorDataset(X_train, y_train), batch_size=32, shuffle=True) test_loader = DataLoader(TensorDataset(X_test, y_test), batch_size=32, shuffle=False)
Теперь мы можем обучить модель, используя следующий цикл обучения с 20 эпохами. Оптимизатор Adam и алгоритм потерь CrossEntropy обычно используются для задач классификации, поэтому мы используем их и здесь:
# настройка модели, потерь и оптимизатора device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = CNSConvNet(num_classes=len(label_encoder.classes_)).to(device) criteria = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # Обучающий цикл num_epochs = 20 for epoch in range(num_epochs): model.train() running_loss = 0.0 for inputs, labels in train_loader: inputs, labels = inputs.to(device), labels.to(device) # Очистить градиенты optimizer.zero_grad() # Прямой проход outputs = model(inputs) loss = criteria(outputs, labels) # Обратный проход и оптимизация loss.backward() optimizer.step() running_loss += loss.item() # Печать статистики print(f'Эпоха {epoch+1}/{num_epochs}, Потеря: {running_loss/len(train_loader):.4f}')
На этом обучение завершено. После этого мы можем оценить модель и распечатать матрицу ошибок:
import numpy as np from sklearn.metrics import confused_matrix import seaborn as sns # Перебираем партии в тестовом наборе и собираем прогнозы model.eval() y_true = [] y_pred = [] with torch.no_grad(): for inputs, labels in test_loader: inputs, labels = inputs.to(device), labels.to(device) outputs = model(inputs) y_true.extend(labels.cpu().numpy()) y_pred.extend(outputs.argmax(dim=1).cpu().numpy()) _, predicted = torch.max(outputs.data, 1) # Рассчитываем точность и матрицу путаницы precision = (np.array(y_true) == np.array(y_pred)).mean() cm = confused_matrix(y_true, y_pred) # Построить график матрица неточностей plt.figure(figsize=(3, 3), dpi=200) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=label_encoder.classes_, yticklabels=label_encoder.classes_) plt.xlabel('Predicted') plt.ylabel('True') plt.title('Матрица неточностей, precision={:.2f}'.format(accuracy)) plt.savefig(«confusion_matrix.png», bbox_inches='tight')

Процесс обучения занимает в общей сложности около 7 секунд на графическом процессоре NVIDIA RTX 4090.
Заключение
Мы разработали эффективный и точный классификатор подтипов рака лёгкого на основе данных о числе копий. Как мы показали, такие модели хорошо переносятся в новые исследования и источники данных о последовательностях.
Массовый ИИ часто оправдывают, в частности, как «решение проблемы рака». Однако, как и в этой статье, небольшие модели с классическими подходами обычно хорошо справляются со своей задачей. Некоторые даже утверждают, что настоящее препятствие машинному обучению в биологии и медицине заключается не в решении проблем, а в реальном влиянии на пациентов.
Тем не менее, машинное обучение в целом смогло решить как минимум одну важнейшую загадку вычислительной биологии, вновь привлекая внимание к машинному обучению в области лечения рака. При некоторой удаче мы, возможно, сможем считать следующее десятилетие временем, когда мы наконец «решили» проблему рака.
Бонус: Cell2Sentence
Современные основополагающие модели часто содержат информацию, например, о том, какие гены имеют высокую копийность в тех или иных видах рака, в составе обучающей выборки. Это, однако, привело к созданию подходов, основанных на LLM, таких как Cell2Sentence, где набор данных преобразуется в текст на естественном языке и отправляется на LLM.
Мы можем использовать этот подход здесь. Сначала мы преобразуем случайную выборку LUSC в ранжированный список генов на основе их копийности:
импорт cns импорт cns.data_utils как cdu samples_df, cns_df = cdu.main_load(«COSMIC») cns_df = cns.add_total_cn(cns_df) lusc_df = cns.select_cns_by_type(cns_df, samples_df, «LUSC») lusc_one_df = cns.cns_head(lusc_df, 1) rank_list = lusc_one_df.sort_values(by=»total_cn», ascending=False)[«name»].to_list()
Затем мы преобразуем список генов в запрос на естественном языке:
query = «Ниже представлен список генов COSMIC {gene_names}, упорядоченный по убыванию общего числа копий в ячейке. Ваша задача — определить тип рака в этом образце.nn» query += «{gene_names}: » + » «.join(rank_list) + «nn» query += «Ответ:» print(query)
Представленная в ChatGPT 5 модель правильно идентифицирует рак LUSC:
Исходя из указанного вами профиля числа копий генов, наиболее убедительным признаком является массивная амплификация 11q13 (CCND1, FADD) вместе с SOX2, PIK3CA, TP63, EGFR, FGFR1, MYC, CCND1/CCND2/CCND3, CDK6, CDK4. Это очень характерный признак плоскоклеточного рака лёгкого (LUSC): амплификация CCND1 — одно из наиболее частых проявлений при LUSC. Амплификация SOX2 (3q26) — определяющий фактор плоскоклеточного рака лёгкого, редко встречающийся при аденокарциноме. Амплификация TP63, опять же, высокоспецифична для плоскоклеточных опухолей. Коамплификация PIK3CA, FGFR1, EGFR, MYC соответствует этому профилю. Широкий набор амплифицированных генов (многие регуляторы плоскоклеточной дифференцировки и сигнальных путей RTK/RAS/PI3K) отражает типичную биологию LUSC, определяемую числом копий. В отличие от этого, в аденокарциномах лёгких обычно наблюдаются точечные мутации/слияния генов KRAS, EGFR, ALK, ROS1, экзона 14 гена MET, ERBB2, а также меньше распространённых CNA плоскоклеточного типа. Ответ: Плоскоклеточный рак лёгких (LUSC).
Однако генерация этого вывода для одного образца занимает больше времени, чем классификация всего набора данных с помощью нашей модели, и на классификацию всего нашего набора данных уйдет около ~200 долларов США в виде платы за API.
Источник: towardsdatascience.com



























