Yuriy Nazarov
Люблю machine learning
Текст википедии используется во множестве экспериментов машинного обучения, но как его получить самостоятельно, например для самой свежей версии или редкого языка?
Достаточно функциональной я нашёл библиотеку mwparserfromhell
Распаршенные данные можно сохранить в текстовых файлах, базе данных или в табличном формате на распределённой файловой системе. Но начать в любом случае нужно с дампа, который находится на dumps.wikimedia.org
Для параллельной обработки любым из способов удобнее будет скачать дамп в виде множества файлов вида enwiki-20200201-pages-articles-multistream1.xml-p10p30302.bz2
Для локальной обработки подойдёт код вида:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import xml.etree.ElementTree as ET from tqdm import tqdm import mwparserfromhell wikidump = "enwiki-20200201-pages-articles-multistream1.xml-p10p30302" doc = {} fields = { "title": "title", "text": "text", } for _, elem in tqdm(ET.iterparse(wikidump, events=("end",))): prefix, has_namespace, postfix = elem.tag.partition('}') tag = postfix if postfix else prefix if tag in fields: doc[fields[tag]] = elem.text if tag == "page": wikicode = mwparserfromhell.parse(doc["text"]) doc["plain_text"] = wikicode.strip_code() doc["wikilinks"] = wikicode.filter_wikilinks() doc["external_links"] = wikicode.filter_external_links() doc["templates"] = wikicode.filter_templates() elem.clear() |
Но mwparserfromhell довольно требовательная к ресурсам библиотека. На обработку всей английской википедии требуется порядка 8 CPU дней. Поэтому воспользуется фреймворком распределённых вычислений Apache Spark.
Для доступности со всех вычислительных нод, следует разместить данные в распределённой файловой системе HDFS. Предварительно распаковав bz2 файлы, чтобы не тратить на это вычисления при каждом чтении.
1 |
hdfs dfs -put enwiki-20200201-pages-articles-multistream*.xml* hdfs://namenode:9000/user/root/wiki/enwiki-20200201/ |
Теперь xml файлы доступны для обработки, но ещё не разделены на отдельные страницы. Сохранить отдельные документы я решил в parquet файлы, которые могут хранить схематизированные таблицы. Для работы с xml я решил воспользоваться библиотекой com.databricks:spark-xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
spark-shell --master spark://sparkmaster:7077 --packages com.databricks:spark-xml_2.12:0.8.0 --executor-memory 12G --executor-cores 8 Spark context available as 'sc' (master = spark://ryzen:7077, app id = app-20200314145829-0001). Spark session available as 'spark'. Welcome to ____ __ / __/__ ___ _____/ /__ _\ \/ _ \/ _ `/ __/ '_/ /___/ .__/\_,_/_/ /_/\_\ version 3.0.0-preview2 /_/ Using Scala version 2.12.10 (OpenJDK 64-Bit Server VM, Java 1.8.0_242) Type in expressions to have them evaluated. Type :help for more information. import org.apache.spark.sql.SparkSession import com.databricks.spark.xml._ spark.read.option("rowTag", "page").xml("hdfs://namenode:9000/user/root/wiki/enwiki-20200201").select("id", "title", "revision.text._VALUE").withColumnRenamed("_VALUE", "text").write.format("parquet").save("hdfs://namenode:9000/user/root/wiki_parsed.parquet") |
Далее из текстов, содержащих wiki разметку требуется выделить чистый текст. В mwparserfromhell для этого есть метод strip_code().
Т.к. не на всех нодах нужная python библиотека может быть предустановлена, выполним дополнительный шаг prepare c pip install —user mwparserfromhell
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
from pyspark.sql import SparkSession from pyspark.sql.functions import * from pyspark.sql.types import ArrayType, StringType, StructType, StructField from pyspark import SparkConf import subprocess import sys import mwparserfromhell def prepare(x): subprocess.check_call([sys.executable, "-m", "pip", "install", "--user", "mwparserfromhell"]) return x def parse_wiki(s): wikicode = mwparserfromhell.parse(s) return ( wikicode.strip_code(), [str(x) for x in wikicode.filter_wikilinks()], [str(x) for x in wikicode.filter_external_links()], [str(x) for x in wikicode.filter_templates()], ) if __name__ == "__main__": hdfs_base = "hdfs://namenode:9000/user/root/" conf = SparkConf().setAll([ ('spark.executor.memory', '12g'), ('spark.executor.cores', '8'), ('spark.hadoop.dfs.replication', '2'), ]) spark = SparkSession.builder.appName("Wiki Word Count").config(conf=conf).getOrCreate() spark.sparkContext.parallelize(range(100)).mapPartitions(prepare).collect() df = spark.read.load(hdfs_base + "wiki_parsed.parquet") my_udf = udf(lambda z: parse_wiki(z if z else ""), StructType([ StructField("plaintext", StringType()), StructField("wikilinks", ArrayType(StringType())), StructField("external_links", ArrayType(StringType())), StructField("templates", ArrayType(StringType())), ])) parsed_df = df.withColumn("parsed", my_udf(df.text)).select( "id", "title", "parsed.plaintext", "parsed.wikilinks", "parsed.external_links", "parsed.templates" ) parsed_df.write.save(hdfs_base + "wiki_mwparserfromhell.parquet", format="parquet") |
Для его запуска нужно выполнить
1 |
spark-submit --master spark://sparkmaster:7077 wiki_plaintext.py |
После выполнения скрипта, можно посмотреть на результат
1 2 3 4 5 6 7 8 9 |
hdfs_base = "hdfs://namenode:9000/user/root/" df = spark.read.load(hdfs_base + "wiki_mwparserfromhell.parquet") df.limit(1).show() +-------+--------------------+--------------------+--------------------+--------------+--------------------+ | id| title| plaintext| wikilinks|external_links| templates| +-------+--------------------+--------------------+--------------------+--------------+--------------------+ |1363416|Calliope (disambi...|Calliope is the m...|[[[Calliope]], [[...| []|[{{Wiktionary|Cal...| +-------+--------------------+--------------------+--------------------+--------------+--------------------+ |
Недавно я заинтересовался темой настройки домашнего hadoop/hdfs/spark кластера, т.к. от апгрейдов осталось какое-то железо, а данные даже дома хочется обрабатывать в парадигме, которая масштабируется.
Вычислительно сложные задачи не всегда упираются даже в 1GbE соединение, но накладные расходы оно всё же вносит. Например, подсчёт слов в английской википедии с локальными данными выполняется за 10 минут, а с данными на соседней ноде — за 13 минут. Для их минимизации решил проапгрейдить сеть.
Доступные 10GbE сетевые адаптеры можно было найти уже давно, но неожиданным для меня оказалось, что 40GbE не дороже. На Авито и eBay Mellanox Connectx-3 mcx354а можно купить за $50.
Из приятного, в ubuntu 18.04 с ядром 5.3 сетевой интерфейс был доступен сразу после загрузки, без какой-либо дополнительной настройки.
Настройки потребовал только mtu. Его увеличение положительно сказывается на скорости.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
$ iperf -c 192.168.3.1 ------------------------------------------------------------ Client connecting to 192.168.3.1, TCP port 5001 TCP window size: 85.0 KByte (default) ------------------------------------------------------------ [ 3] local 192.168.3.2 port 42464 connected with 192.168.3.1 port 5001 [ ID] Interval Transfer Bandwidth [ 3] 0.0-10.0 sec 35.9 GBytes 30.8 Gbits/sec $ sudo ifconfig enp1s0d1 192.168.3.2 mtu 8000 up $ iperf -c 192.168.3.1 ------------------------------------------------------------ Client connecting to 192.168.3.1, TCP port 5001 TCP window size: 325 KByte (default) ------------------------------------------------------------ [ 3] local 192.168.3.2 port 44338 connected with 192.168.3.1 port 5001 [ ID] Interval Transfer Bandwidth [ 3] 0.0-10.0 sec 38.9 GBytes 33.5 Gbits/sec |
А с настройкой mtu при загрузке возникли сложности, которые уже зарепорчены в https://bugs.launchpad.net/netplan/+bug/1724895
После применения рецепта из тикета, конфиг стал такой:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
$ cat /etc/netplan/01-netcfg.yaml network: version: 2 renderer: networkd ethernets: enp1s0: dhcp4: no addresses: [192.168.2.2/24] mtu: 8000 match: macaddress: 00:10:e0:4a:18:31 enp1s0d1: dhcp4: no addresses: [192.168.3.2/24] mtu: 8000 match: macaddress: 00:10:e0:4a:18:32 |
И можно не переживать о деградации производительности после перезагрузки.
До недавнего времени я занимался исключительно разработкой програмного и немного аппаратного обеспечения, но смена места работы подтолкнула меня открыть для себя целый новый мир машинного обучения. Хоть я и был знаком с реализацией некоторых методов ML ещё с университета, но осознание масштабов его применения стало для меня открытием.
Приведу нуже ссылки материалов, которые посчитал полезными:
coursera.org/learn/vvedenie-mashinnoe-obuchenie — курс по ML, который отлично «вправляет мозги»
rusvectores.org/ru/ — word2vec модели для русского языка
tensorflow.org/tutorials/ — туториалы к библиотеке по ML, которые объясняют почему модели работают. Например отличный разбор word2vec.
scikit-learn.org — swish army knife дата саентиста. Для тех, кто хочет чтобы «просто работало». До меня медленно доходило, что применение нового инструмента часто приносит больше пользы, чем мучительное допиливание старого. Т.е. для проверки гипотез рекомендовал бы именно эту библиотеку вместо более низкоуровневых и дающих больше контроля вроде tensorflow.
Читать далее
Возможно вы просто искали способ эмуляции нажатия клавиши в приложении, и хотели найти SendMessage(WM_KEYDOWN)+SendMessage(WM_KEYUP) или SendInput(), но если вы уже столкнулись с тем, что этот способ вам не подходит, как, например, в играх, которые специально защищаются от подобных методов для усложнения жизни ботописателям, добро пожаловать под кат.
При реализации проекта DistTest возникла необходимость собирать ядро linux из разных версий исходников. И первым решением было загружать готовые архивы исходников с kernel.org для каждой отдельной версии. Но вскоре стало понятно, что данный подход требует значительного дискового пространства: 0.5-1ГБ на каждую версию, которых в данный момент порядка 1000. А также данные подход позволяет собирать только именованные версии, но не версии соответствующие отдельным коммитам.
Для решения проблемы дискового пространства можно хранить несколько базовых версий и приводить их к необходимому виду с помощью патчей, но это приводит к значительному объёму I/O со случайным доступом, что выполняется медленно на HDD и приводит к износу SSD. Тот факт, что исходники необходимой версии требуются лишь временно, подсказывает использовать tmpfs. Но есть более эффективный подход в плане использования оперативной памяти — хранить в tmpfs только отличающиеся от уже имеющихся файлов, что позволяет сделать aufs.
Запуск виртуальной машины с последовательным портом в качесте консоли и перенаправлением его в stdio процесса qemu:
1 |
qemu-system-i386 -M pc-1.0 -m 64 -kernel $kernel_path -initrd $initrd_path -append 'console=tty0 console=ttyS0,115200n8' -chardev stdio,id=charserial0 -device isa-serial,chardev=charserial0,id=serial0 -nographic -nodefconfig -nodefaults |
Для примера работы эмулятора будем использовать дистрибутив tinycore linux:
Пересборка initrd образа:
1 2 3 4 5 6 7 8 9 |
gunzip core.gz mkdir initrd cd initrd cpio -iv < ../core perl -pi -e 's/tty1/ttyS0/g' etc/inittab perl -pi -e 's/# ttyS0/ttyS0/g' etc/securetty find * -print0 | cpio -ov0 -H newc >../core cd .. gzip core |
Пример вывода(перенаправление консоли ядра отключено для наглядности):
1 2 3 4 5 6 7 8 9 |
$ echo ' sudo poweroff' | qemu-system-i386 -M pc-1.0 -m 64 -kernel /media/data/qemu/vmlinuz -initrd /media/data/qemu/core.gz -chardev stdio,id=charserial0 -device isa-serial,chardev=charserial0,id=serial0 -nographic -nodefconfig -nodefaults | sed 's~\[J~~' | sed 's~\[H~~' (�- //\ Core is distributed with ABSOLUTELY NO WARRANTY. v_/_ www.tinycorelinux.com tc@box:~$ sudo poweroff tc@box:~$ $ |
Когда приложение интенсивно создаёт и удаляет различные объекты при этом не производя над ними каких-либо ресурсоёмких вычислений, производительность приложения ограничивается в основном подсистемой управления памятью. И для более аккуратной разработки приложения необходимо воспринимать эту подсистему уже не как «чёрный ящик», а уже учитывать некоторые его особенности. Рассмотрим аллокатор из библиотеки glibc на платформе linux x86_64.
Какое же API предоставляет аллокатор из glibc? Как он работает? Какие неожиданности в себе таит? Читать далее
Во время поиска документации по незнакомым функциям стандартной библиотеки, наткнулся на удивительную фичу Perl’а — source filters. Она позволяет выполнять препроцессинг исходного кода перед выполнением. Обработка может производится как кодом на C, так и кодом на самом Perl’е, что является наиболее переносимым вариантом.
Так чего же он нам позволяет добиться?
Многого! Например можно добавить возможность использовать прототипы функций с именованными аргументами, которые появятся только в Perl6.
1 2 3 4 5 6 7 8 |
#!/usr/bin/perl use strict; use FunctionPrototype; sub myfunction($c, $d){ print "c: $c, d: $d\n"; } myfunction("arg1", "arg2"); |
Или изменить синтаксис чуть сильнее:
1 2 3 4 5 6 7 8 9 10 11 |
#!/usr/bin/perl use JavaLikePerl; /** * The HelloWorldApp class implements an application that * simply prints "Hello World!" to standard output. */ class HelloWorldApp { public static void main(String[] args) { System.out.println("Hello World!"); // Display the string. } } |
Ну, во-первых, файл очень часто занимает отличное от своего размера место
В большинстве файловых систем Linux место не освободится пока файл не закроют все процессы, работающие с ним.
Вот в hfsplus в Linux 2.6.32-2.6.36 не так. В 2.6.37 это таки пофиксили.
Много чудных файловых систем можно встретить. Вот например nilfs
1 2 3 4 5 6 7 8 9 10 11 |
$ df Filesystem 1K-blocks Used Available Use% Mounted on /dev/loop0 253948 16380 172032 9% /tmp/nilfs $ dd if=/dev/zero of=/tmp/nilfs/123 bs=1K count=10000 $ dd if=/dev/zero of=/tmp/nilfs/123 bs=1K count=10000 $ dd if=/dev/zero of=/tmp/nilfs/123 bs=1K count=10000 $ df /dev/loop0 253948 32764 155648 18% /tmp/nilfs $ rm /tmp/nilfs/123 $ df /dev/loop0 253948 40956 147456 22% /tmp/nilfs |
Файловые системы со сжатием могут сжать новый файл лучше.
Файлы кто-то может переименовать (c)ваш К.О. Даже в самый неожиданный момент (c)всё тот же К.О. Читать далее