#
Sắp xếp dữ liệu

Hầu hết thời gian, dữ liệu bạn làm việc sẽ chưa được chuẩn bị hoàn hảo cho các mô hình huấn luyện. Trong phần này, chúng ta sẽ khám phá các tính năng khác nhau mà 🤗 Datasets cung cấp để làm sạch các tập dữ liệu của bạn.

##
Sắp xếp dữ liệu của chúng ta

Tương tự như Pandas, 🤗 Datasets cung cấp một số tính năng để thao túng nội dung của `Dataset` và `DatasetDict`. Chúng ta đã gặp phương thức `Dataset.map()` trong Chương 3 và trong phần này, chúng ta sẽ khám phá một số hàm khác theo ý của chúng ta.

Đối với ví dụ này, chúng tôi sẽ sử dụng Bộ dữ liệu đánh giá thuốc được lưu trữ trên Kho lưu trữ Học máy UC Irvine, chứa các đánh giá của bệnh nhân về các loại thuốc khác nhau, cùng với tình trạng đang được điều trị và xếp hạng 10 sao về mức độ hài lòng của bệnh nhân.

Trước tiên, chúng ta cần tải xuống và giải nén dữ liệu, có thể thực hiện bằng lệnh `wget` và `unzip`:

    !wget "https://archive.ics.uci.edu/ml/machine-learning-databases/00462/drugsCom_raw.zip"
    !unzip drugsCom_raw.zip

Vì TSV chỉ là một biến thể của CSV sử dụng dấu tab thay vì dấu phẩy làm dấu phân cách, chúng ta có thể tải các tệp này bằng cách sử dụng tập lệnh tải `csv` và chỉ định đối số `delimiter` trong hàm `load_dataset()` như sau:

    from datasets import load_dataset

    data_files = {"train": "drugsComTrain_raw.tsv", "test": "drugsComTest_raw.tsv"}
    # \t is the tab character in Python
    drug_dataset = load_dataset("csv", data_files=data_files, delimiter="\t")

Một thực tiễn khi thực hiện bất kỳ loại phân tích dữ liệu nào là lấy một mẫu ngẫu nhiên nhỏ để có thể cảm nhận nhanh về loại dữ liệu bạn đang làm việc. Trong 🤗 Datasets, chúng ta có thể tạo một mẫu ngẫu nhiên bằng cách xâu chuỗi các hàm `Dataset.shuffle()` và `Dataset.select()` với nhau:

    drug_sample = drug_dataset["train"].shuffle(seed=42).select(range(1000))
    # Xem qua một số ví dụ đầu tiên
    drug_sample[:3]

    {'Unnamed: 0': [87571, 178045, 80482],
     'drugName': ['Naproxen', 'Duloxetine', 'Mobic'],
     'condition': ['Gout, Acute', 'ibromyalgia', 'Inflammatory Conditions'],
     'review': ['"like the previous person mention, I&#039;m a strong believer of aleve, it works faster for my gout than the prescription meds I take. No more going to the doctor for refills.....Aleve works!"',
      '"I have taken Cymbalta for about a year and a half for fibromyalgia pain. It is great\r\nas a pain reducer and an anti-depressant, however, the side effects outweighed \r\nany benefit I got from it. I had trouble with restlessness, being tired constantly,\r\ndizziness, dry mouth, numbness and tingling in my feet, and horrible sweating. I am\r\nbeing weaned off of it now. Went from 60 mg to 30mg and now to 15 mg. I will be\r\noff completely in about a week. The fibro pain is coming back, but I would rather deal with it than the side effects."',
      '"I have been taking Mobic for over a year with no side effects other than an elevated blood pressure.  I had severe knee and ankle pain which completely went away after taking Mobic.  I attempted to stop the medication however pain returned after a few days."'],
     'rating': [9.0, 3.0, 10.0],
     'date': ['September 2, 2015', 'November 7, 2011', 'June 5, 2013'],
     'usefulCount': [36, 13, 128]}

Lưu ý rằng chúng ta đã sửa seed trong `Dataset.shuffle()` cho mục đích tái tạo. `Dataset.select()` mong đợi một chỉ số có thể lặp lại, vì vậy chúng ta truyền vào khoảng `range(1000)` để lấy 1,000 mẫu đầu tiên từ tập dữ liệu đã xáo trộn. Từ mẫu này, ta đã có thể thấy một số điều kỳ quặc trong tập dữ liệu:

- Cột `Unnamed: 0` trông đáng ngờ giống như một ID ẩn danh cho mỗi bệnh nhân.
- Cột `condition` bao gồm sự kết hợp giữa các nhãn chữ hoa và chữ thường.
- Các bài đánh giá có độ dài khác nhau và chứa hỗn hợp các dấu phân tách dòng Python (`\r\n`) cũng như các mã ký tự HTML như `&\#039;`.

Hãy xem cách chúng ta có thể sử dụng 🤗 Datasets để giải quyết từng vấn đề này. Để kiểm tra giả thuyết ID bệnh nhân cho cột `Unnamed: 0`, ta có thể sử dụng hàm `Dataset.unique()` để xác minh rằng số lượng ID khớp với số hàng trong mỗi lần tách:

    for split in drug_dataset.keys():
        assert len(drug_dataset[split]) == len(drug_dataset[split].unique("Unnamed: 0"))

Điều này dường như xác nhận giả thuyết của chúng tôi, vì vậy hãy dọn dẹp tập dữ liệu một chút bằng cách đổi tên cột `Unname: 0` thành một cái gì đó dễ hiểu hơn một chút. Chúng ta có thể sử dụng hàm `DatasetDict.rename_column()` để đổi tên cột trên cả hai tập con trong một lần:

    drug_dataset = drug_dataset.rename_column(
        original_column_name="Unnamed: 0", new_column_name="patient_id"
    )
    drug_dataset

    DatasetDict({
        train: Dataset({
            features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount'],
            num_rows: 161297
        })
        test: Dataset({
            features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount'],
            num_rows: 53766
        })
    })

✏️ Thử nghiệm thôi! Sử dụng hàm `Dataset.unique()` để tìm số lượng thuốc độc nhất và điều kiện trong tập huấn luyện và kiểm thử.

Tiếp theo, hãy chuẩn hóa tất cả các nhãn `condition` bằng cách sử dụng `Dataset.map()`. Như chúng ta đã làm với tokenize trong Chương 3, chúng ta có thể xác định một hàm đơn giản có thể được áp dụng trên tất cả các hàng của mỗi tập trong `drug_dataset`:

    def lowercase_condition(example):
        return {"condition": example["condition"].lower()}

    drug_dataset.map(lowercase_condition)

    AttributeError: 'NoneType' object has no attribute 'lower'

Ồ không, chúng ta đã gặp sự cố với chức năng nối của mình! Từ lỗi, chúng ta có thể suy ra rằng một số mục nhập trong cột `condition` là `None`, không thể viết thường vì chúng không phải là chuỗi. Hãy bỏ các hàng này bằng cách sử dụng `Dataset.filter()`, hoạt động theo cách tương tự như `Dataset.map()` và mong đợi một hàm nhận được một mẫu về tập dữ liệu. Thay vì viết một hàm rõ ràng như:

    def filter_nones(x):
        return x["condition"] is not None

và sau đó chạy `drug_dataset.filter(filter_nones)`, chúng ta có thể thực hiện việc này trong một dòng bằng cách sử dụng hàm lambda. Trong Python, các hàm lambda là các hàm nhỏ mà bạn có thể định nghĩa mà không cần đặt tên rõ ràng. Chúng có dạng chung:

    lambda <arguments> : <expression>

ở đây `lambda` là một trong những từ khóa đặc biệt của Python, `<arguments>` là danh sách / tập hợp các giá trị được phân tách bằng dấu phẩy xác định các đầu vào cho hàm và `<expression>` đại diện cho các hoạt động bạn muốn thực hiện. Ví dụ, chúng ta có thể định nghĩa một hàm lambda đơn giản bình phương một số như sau:

    lambda x : x * x

Để áp dụng hàm này cho một đầu vào, chúng ta cần đặt nó và đầu vào trong dấu ngoặc đơn:

    (lambda x: x * x)(3)

    9

Tương tự, chúng ta có thể định nghĩa các hàm lambda với nhiều tham số bằng cách phân tách chúng bằng dấu phẩy. Ví dụ, chúng ta có thể tính diện tích của một tam giác như sau:

    (lambda base, height: 0.5 * base * height)(4, 8)

    16.0

Các hàm Lambda rất hữu ích khi bạn muốn định nghĩa các hàm nhỏ, sử dụng một lần (để biết thêm thông tin về chúng, chúng tôi khuyên bạn nên đọc Hướng dẫn Python đích thực của Andre Burgaud). Trong ngữ cảnh 🤗 Datasets, chúng ta có thể sử dụng các hàm lambda để xác định các hoạt động nối và lọc đơn giản, vì vậy hãy sử dụng thủ thuật này để loại bỏ các phần `None` trong tập dữ liệu:

    drug_dataset = drug_dataset.filter(lambda x: x["condition"] is not None)

Với `None` đã bị xóa, chúng ta có thể chuẩn hóa cột `condition`:

    drug_dataset = drug_dataset.map(lowercase_condition)
    # Kiểm tra xem chữ viết thường đã hoạt động chưa
    drug_dataset["train"]["condition"][:3]

    ['left ventricular dysfunction', 'adhd', 'birth control']

Nó hoạt động! Vậy là chúng ta đã làm sạch các nhãn, giờ chúng ta hãy xem xét việc làm sạch các bài đánh giá.

##
Tạo ra các cột mới

Bất cứ khi nào bạn xử lý các bài đánh giá của khách hàng, một phương pháp hay đó là kiểm tra số lượng từ trong mỗi bài đánh giá. Bài đánh giá có thể chỉ là một từ duy nhất như “Tuyệt vời!” hoặc một bài luận đầy đủ với hàng nghìn từ, và tùy thuộc vào trường hợp sử dụng, bạn sẽ cần xử lý những thái cực này theo cách khác nhau. Để tính toán số lượng từ trong mỗi bài đánh giá, chúng tôi sẽ sử dụng phương pháp phỏng đoán sơ bộ dựa trên việc tách từng văn bản theo khoảng trắng.

Hãy định nghĩa một hàm đơn giản đếm số từ trong mỗi bài đánh giá:

    def compute_review_length(example):
        return {"review_length": len(example["review"].split())}

Không giống như hàm `lowercase_condition()`, `compute_review_length()` trả về một từ điển có khóa không tương ứng với một trong các tên cột trong tập dữ liệu. Trong trường hợp này, khi `compute_review_length()` được truyền vào `Dataset.map()`, nó sẽ được áp dụng cho tất cả các hàng trong tập dữ liệu để tạo cột mới `review_length`:

    drug_dataset = drug_dataset.map(compute_review_length)
    # Kiểm tra mẫu huấn luyện đầu tiên
    drug_dataset["train"][0]

    {'patient_id': 206461,
     'drugName': 'Valsartan',
     'condition': 'left ventricular dysfunction',
     'review': '"It has no side effect, I take it in combination of Bystolic 5 Mg and Fish Oil"',
     'rating': 9.0,
     'date': 'May 20, 2012',
     'usefulCount': 27,
     'review_length': 17}

Như mong đợi, chúng ta có thể thấy cột `review_length` đã được thêm vào tập huấn luyện của chúng ta. Chúng ta có thể sắp xếp cột mới này với `Dataset.sort()` để xem các giá trị cực đại trông như thế nào:

    drug_dataset["train"].sort("review_length")[:3]

    {'patient_id': [103488, 23627, 20558],
     'drugName': ['Loestrin 21 1 / 20', 'Chlorzoxazone', 'Nucynta'],
     'condition': ['birth control', 'muscle spasm', 'pain'],
     'review': ['"Excellent."', '"useless"', '"ok"'],
     'rating': [10.0, 1.0, 6.0],
     'date': ['November 4, 2008', 'March 24, 2017', 'August 20, 2016'],
     'usefulCount': [5, 2, 10],
     'review_length': [1, 1, 1]}

Như ta đã nghi vấn, một số đánh giá chỉ chứa một từ duy nhất, mặc dù có thể ổn để phân tích sắc thái, nhưng sẽ không có nhiều thông tin nếu chúng tôi muốn dự đoán tình trạng bệnh.

🙋 Một cách thay thế để thêm các cột mới vào tập dữ liệu là sử dụng hàm `Dataset.add_column()`. Điều này cho phép bạn cung cấp cột dưới dạng danh sách Python hoặc mảng NumPy và có thể hữu ích trong các trường hợp mà `Dataset.map()` không phù hợp cho phân tích của bạn.

Hãy sử dụng hàm `Dataset.filter()` để xóa các bài đánh giá có ít hơn 30 từ. Tương tự như những gì chúng ta đã làm với cột `condition`, chúng ta có thể lọc ra các bài đánh giá rất ngắn bằng cách yêu cầu các bài đánh giá có độ dài trên ngưỡng này:

    drug_dataset = drug_dataset.filter(lambda x: x["review_length"] > 30)
    print(drug_dataset.num_rows)

    {'train': 138514, 'test': 46108}

Như bạn có thể thấy, điều này đã loại bỏ khoảng 15% bài đánh giá khỏi bộ huấn luyện và kiểm thử ban đầu.

✏️ Thử nghiệm thôi! Sử dụng hàm `Dataset.sort()` để kiểm tra các bài đánh giá có số lượng từ lớn nhất. Tham khảo tài liệu để biết bạn cần sử dụng tham số nào để sắp xếp các bài đánh giá theo thứ tự giảm dần.

Điều cuối cùng chúng ta cần giải quyết là sự hiện diện của ký tự HTML trong các bài đánh giá của chúng ta. Chúng ta có thể sử dụng mô-đun `html` của Python để loại bỏ qua các ký tự này, như sau:

    import html

    text = "I&#039;m a transformer called BERT"
    html.unescape(text)

    "I'm a transformer called BERT"

Ta sẽ sử dụng `Dataset.map()` để hủy tất cả các ký tự HTML trong kho tài liệu của mình:

    drug_dataset = drug_dataset.map(lambda x: {"review": html.unescape(x["review"])})

Như bạn có thể thấy, phương thức `Dataset.map()` khá hữu ích để xử lý dữ liệu - và chúng ta thậm chí còn chưa rõ tất mọi thứ mà nó có thể làm!

##
Siêu sức mạnh của hàm `map()`

Phương thức `Dataset.map ()` nhận tham số `batched`, nếu được đặt thành `True`, nó sẽ gửi một loạt các mẫu đến hàm map cùng một lúc (ta có thể cấu hình kích thước lô nhưng mặc định là 1,000). Ví dụ: hàm map trước đó loại bỏ tất cả HTML đã mất một chút thời gian để chạy (bạn có thể đọc thời gian thực hiện từ các thanh tiến trình). Chúng ta có thể tăng tốc độ này bằng cách xử lý một số phần tử cùng lúc thông qua sử dụng bao hàm.

Khi bạn chỉ định `batched=True`, hàm sẽ nhận một từ điển với các trường của tập dữ liệu, nhưng mỗi giá trị bây giờ là một danh sách các giá trị và không chỉ là một giá trị duy nhất. Giá trị trả về của `Dataset.map()` phải giống nhau: một từ điển với các trường ta muốn cập nhật hoặc thêm vào tập dữ liệu của mình và một danh sách các giá trị. Ví dụ: đây là một cách khác để hủy tất cả các ký tự HTML, nhưng sử dụng `batched=True`:

    new_drug_dataset = drug_dataset.map(
        lambda x: {"review": [html.unescape(o) for o in x["review"]]}, batched=True
    )

Nếu bạn đang chạy đoạn mã này trên notebook, bạn sẽ thấy rằng lệnh này thực thi nhanh hơn lệnh trước đó. Và đó không phải là do các bài đánh giá của chúng tôi đã được loại đi HTML - nếu bạn thực hiện lại hướng dẫn từ phần trước (không có `batch = True`), nó sẽ mất cùng một khoảng thời gian như trước. Điều này là do việc bao hàm thường nhanh hơn việc thực thi cùng một đoạn mã trong vòng lặp `for` và chúng ta cũng đạt được một số hiệu suất bằng cách truy cập nhiều phần tử cùng một lúc thay vì từng phần tử một.

Sử dụng `Dataset.map()` với `batched=True` sẽ là điều cần thiết để mở khóa tốc độ của các trình tokenize “nhanh” mà chúng ta sẽ gặp trong Chương 6, có thể nhanh chóng tokenize các danh sách lớn các văn bản. Ví dụ: để tokenize tất cả các đánh giá thuốc bằng trình tokenize nhanh, ta có thể sử dụng một hàm như sau:

    from transformers import AutoTokenizer

    tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")

    def tokenize_function(examples):
        return tokenizer(examples["review"], truncation=True)

Như bạn đã thấy trong Chương 3, chúng ta có thể truyền vào một hoặc nhiều mẫu cho tokenizer, vì vậy ta có thể sử dụng hàm này với `batched=True` hoặc không. Hãy cũng coi đây là một cơ hội để so sánh hiệu năng của hai tuỳ chọn này. Trong một notebook, bạn có thể bấm giờ chỉ với một dòng lệnh `%time` trước dòng mã bạn muốn tình thời gian:

    %time tokenized_dataset = drug_dataset.map(tokenize_function, batched=True)

Bạn cũng có thể tính thời gian cho toàn bộ ô bằng cách đặt `%%time` ở đầu của ô mã. Trên phần cứng mà chúng ta thực hiện, nó hiển thị 10.8 giây cho lệnh này (đó là số được viết sau “Wall time”).

✏️ Thử nghiệm thôi! Thực hiện cùng một hướng dẫn có và không có `batched=True`, sau đó thử nó với tokenizer chậm (thêm `use_fast=False` vào `AutoTokenizer.from_pretrained()`) để bạn có thể thấy giá trị bạn nhận được trên phần cứng của mình.

Dưới đây là kết quả thu được khi có và không có tính năng phân lô, với tokenizer nhanh và chậm:

Tuỳ chọn | Tokenizer nhanh | Tokenizer chậm
--- | --- | ---
`batched=True` | 10.8s | 4min41s
`batched=False` | 59.2s | 5min3s

Điều này có nghĩa là việc sử dụng một tokenizer nhanh với tùy chọn `batched=True` sẽ nhanh hơn 30 lần so với phiên bản chậm mà không có lô - điều này thực sự tuyệt vời! Đó là lý do chính tại sao tokenizer nhanh là mặc định khi sử dụng `AutoTokenizer` (và tại sao chúng được gọi là “nhanh”). Chúng có thể đạt được tốc độ như vậy bởi vì phía sau, đoạn mã token hóa được thực thi bằng Rust, đây là một ngôn ngữ giúp dễ dàng thực hiện đoạn mã song song.

Song song hóa cũng là lý do giải thích cho tốc độ tăng gần gấp 6 lần mà trình tokenize nhanh đạt được với việc phân lô: bạn không thể song song một thao tác tokenize đơn lẻ, nhưng khi bạn muốn tokenize nhiều văn bản cùng một lúc, bạn có thể chỉ cần chia nhỏ việc thực thi trên nhiều quy trình, mỗi người chịu trách nhiệm về các văn bản của riêng mình.

`Dataset.map()` cũng tự có một số khả năng tính toán song song. Vì chúng không được hỗ trợ bởi Rust, nên chúng sẽ không để một trình tokenizer chậm bắt kịp với một tokenizer nhanh, nhưng chúng vẫn có thể hữu ích (đặc biệt nếu bạn đang sử dụng một tokenizer không có phiên bản nhanh). Để bật xử lý đa luồng, hãy sử dụng tham số `num_proc` và chỉ định số lượng quy trình sẽ sử dụng trong lệnh gọi của bạn tới `Dataset.map()`:

    slow_tokenizer = AutoTokenizer.from_pretrained("bert-base-cased", use_fast=False)

    def slow_tokenize_function(examples):
        return slow_tokenizer(examples["review"], truncation=True)

    tokenized_dataset = drug_dataset.map(slow_tokenize_function, batched=True, num_proc=8)

Bạn có thể thử nghiệm một chút với thời gian để xác định số lượng quy trình tối ưu để sử dụng; trong trường hợp của chúng ta, 8 dường như tạo ra tốc độ tăng tốt nhất. Dưới đây là những con số chúng tôi nhận được khi có và không có xử lý đa luồng:

Tuỳ chọn | Tokenizer nhanh | Tokenizer chậm
--- | --- | ---
`batched=True` | 10.8s | 4min41s
`batched=False` | 59.2s | 5min3s
`batched=True`, `num_proc=8` | 6.52s | 41.3s
`batched=False`, `num_proc=8` | 9.49s | 45.2s

Đó là những kết quả hợp lý hơn nhiều đối với tokenizer chậm, nhưng hiệu suất của tokenizer nhanh cũng đã được cải thiện đáng kể. Tuy nhiên, lưu ý rằng điều đó không phải lúc nào cũng đúng - đối với các giá trị của `num_proc` khác 8, các thử nghiệm của chúng tôi cho thấy rằng sử dụng `batched=True` mà không có tùy chọn này sẽ nhanh hơn. Nói chung, chúng tôi khuyên bạn không nên sử dụng xử lý đa luồng Python cho các trình tokenize nhanh với `batched=True`.

Sử dụng `num_proc` để tăng tốc quá trình xử lý của bạn thường là một ý tưởng tuyệt vời, miễn là hàm bạn đang sử dụng chưa thực hiện một số kiểu xử lý đa xử lý của riêng nó.

Tất cả các chức năng này được cô đọng trong một phương pháp đã khá tuyệt vời, nhưng còn nhiều hơn thế nữa! Với `Dataset.map()` và `batched=True`, bạn có thể thay đổi số lượng phần tử trong tập dữ liệu của mình. Điều này cực kỳ hữu ích trong nhiều trường hợp mà bạn muốn tạo một số đặc trưng huấn luyện từ một mẫu và chúng ta sẽ cần thực hiện điều này như một phần của quá trình tiền xử lý cho một số tác vụ NLP sẽ thực hiện trong Chương 7.

💡 Trong học máy, một mẫu thường được định nghĩa là tập hợp đặc trưng mà chúng ta cung cấp cho mô hình. Trong một số ngữ cảnh, các đặc trưng này sẽ là tập hợp thành các cột trong `Dataset`, nhưng trong các trường hợp khác (như ở đây và để phục vụ hỏi đáp), nhiều đặc trưng có thể được trích xuất từ một mẫu và thuộc về một cột duy nhất.

Chúng ta hãy xem nó hoạt động như thế nào! Ở đây, ta sẽ tokenize các mẫu của mình và cắt chúng về độ dài tối đa là 128, nhưng ta sẽ yêu cầu trình tokenize trả về tất cả các đoạn văn bản thay vì chỉ đoạn văn bản đầu tiên. Điều này có thể được thực hiện với `return_overflowing_tokens=True`:

    def tokenize_and_split(examples):
        return tokenizer(
            examples["review"],
            truncation=True,
            max_length=128,
            return_overflowing_tokens=True,
        )

Hãy kiểm tra điều này trên một mẫu trước khi sử dụng `Dataset.map()` trên toàn bộ tập dữ liệu:

    result = tokenize_and_split(drug_dataset["train"][0])
    [len(inp) for inp in result["input_ids"]]

    [128, 49]

Vì vậy, mẫu đầu tiên trong tập huấn luyện đã trở thành hai đặc trưng vì nó đã được tokenize nhiều hơn số lượng token tối đa mà chúng tôi đã chỉ định: cái đầu tiên có độ dài 128 và cái thứ hai có độ dài 49. Bây giờ hãy làm điều này cho tất cả các phần tử của tập dữ liệu!

    tokenized_dataset = drug_dataset.map(tokenize_and_split, batched=True)

    ArrowInvalid: Column 1 named condition expected length 1463 but got length 1000

Ôi không! Nó đã không hoạt động! Tại sao không? Nhìn vào thông báo lỗi sẽ cho chúng ta manh mối: có sự không khớp về độ dài của một trong các cột, một cột có độ dài 1,463 và cột còn lại có độ dài 1,000. Nếu bạn đã xem tài liệu về `Dataset.map()`, bạn có thể nhớ rằng đó là số các mẫu được truyền vào hàm mà chúng ta đang ánh xạ; ở đây 1,000 mẫu đó đã cung cấp 1,463 đặc trưng mới, dẫn đến lỗi hình dạng.

Vấn đề là chúng ta đang cố gắng kết hợp hai tập dữ liệu khác nhau với các kích thước khác nhau: cột `drug_dataset` sẽ có một số mẫu nhất định (lỗi phía chúng ta là 1,000), nhưng `tokenized_dataset` mà chúng ta đang xây dựng sẽ có nhiều hơn (1,463 trong thông báo lỗi). Điều này không hoạt động đối với `Dataset`, vì vậy chúng ta cần xóa các cột khỏi tập dữ liệu cũ hoặc làm cho chúng có cùng kích thước với chúng trong tập dữ liệu mới. Chúng ta có thể thực hiện điều đầu thông qua tham số `remove_columns`:

    tokenized_dataset = drug_dataset.map(
        tokenize_and_split, batched=True, remove_columns=drug_dataset["train"].column_names
    )

Bây giờ nó hoạt động mà không có lỗi. Chúng ta có thể kiểm tra xem tập dữ liệu mới của mình có nhiều phần tử hơn tập dữ liệu gốc hay không bằng cách so sánh độ dài:

    len(tokenized_dataset["train"]), len(drug_dataset["train"])

    (206772, 138514)

Chúng tôi đã đề cập rằng chúng ta cũng có thể giải quyết vấn đề chiều dài không khớp bằng cách làm cho các cột cũ có cùng kích thước với các cột mới. Để thực hiện việc này, chúng ta sẽ cần trường `overflow_to_sample_mapping` mà tokenizer trả về khi chúng ta đặt `return_overflowing_tokens=True`. Nó cung cấp cho chúng ta một ánh xạ từ một chỉ mục đặc trưng mới đến chỉ mục của mẫu mà nó bắt nguồn từ đó. Sử dụng điều này, chúng ta có thể liên kết mỗi khóa có trong tập dữ liệu ban đầu với một danh sách các giá trị có kích thước phù hợp bằng cách lặp lại các giá trị của mỗi ví dụ nhiều lần khi nó tạo ra các đặc trưng mới:

    def tokenize_and_split(examples):
        result = tokenizer(
            examples["review"],
            truncation=True,
            max_length=128,
            return_overflowing_tokens=True,
        )
        # Extract mapping between new and old indices
        sample_map = result.pop("overflow_to_sample_mapping")
        for key, values in examples.items():
            result[key] = [values[i] for i in sample_map]
        return result

Chúng ta có thể thấy nó hoạt động với `Dataset.map()` mà chúng ta không cần xóa các cột cũ:

    tokenized_dataset = drug_dataset.map(tokenize_and_split, batched=True)
    tokenized_dataset

    DatasetDict({
        train: Dataset({
            features: ['attention_mask', 'condition', 'date', 'drugName', 'input_ids', 'patient_id', 'rating', 'review', 'review_length', 'token_type_ids', 'usefulCount'],
            num_rows: 206772
        })
        test: Dataset({
            features: ['attention_mask', 'condition', 'date', 'drugName', 'input_ids', 'patient_id', 'rating', 'review', 'review_length', 'token_type_ids', 'usefulCount'],
            num_rows: 68876
        })
    })

Chúng ta nhận được cùng số đặc trưng huấn luyện như trước đó, nhưng ở đây ta đã giữ lại tất cả các trường cũ. Nếu bạn cần chúng để hậu xử lý sau khi áp dụng mô hình của mình, bạn có thể muốn sử dụng phương pháp này.

Bây giờ bạn đã thấy cách 🤗 Datasets có thể được sử dụng để tiền xử lý một tập dữ liệu theo nhiều cách khác nhau. Mặc dù các chức năng xử lý của 🤗 Datasets sẽ đáp ứng hầu hết các nhu cầu huấn luyện mô hình của bạn, có thể đôi khi bạn cần chuyển sang Pandas để truy cập các tính năng mạnh mẽ hơn, chẳng hạn như `DataFrame.groupby()` hoặc các API cấp cao để trực quan hóa. May mắn thay, 🤗 Datasets được thiết kế để có thể tương tác với các thư viện như Pandas, NumPy, PyTorch, TensorFlow và JAX. Chúng ta hãy xem cách này hoạt động như thế nào.

##
Từ `Dataset` tới `DataFrame` và ngược lại

Để cho phép chuyển đổi giữa các thư viện bên thứ ba khác nhau, 🤗 Datasets cung cấp hàm `Dataset.set_format()`. Hàm này chỉ thay đổi định dạng đầu ra của tập dữ liệu, vì vậy bạn có thể dễ dàng chuyển sang định dạng khác mà không ảnh hưởng đến định dạng đầu ra bên dưới, đó là Apache Arrow. Việc định dạng được thực hiện tại chỗ. Để chứng minh, hãy chuyển đổi tập dữ liệu của chúng tôi thành Pandas:

    drug_dataset.set_format("pandas")

Giờ khi chúng ta truy cập các phần tử của tập dữ liệu, ta nhận được `pandas.DataFrame` thay vì từ điển:

    drug_dataset["train"][:3]

| patient_id | drugName | condition | review | rating | date | usefulCount | review_length
--- | --- | --- | --- | --- | --- | --- | --- | ---
0 | 95260 | Guanfacine | adhd | "My son is halfway through his fourth week of Intuniv..." | 8.0 | April 27, 2010 | 192 | 141
1 | 92703 | Lybrel | birth control | "I used to take another oral contraceptive, which had 21 pill cycle, and was very happy- very light periods, max 5 days, no other side effects..." | 5.0 | December 14, 2009 | 17 | 134
2 | 138000 | Ortho Evra | birth control | "This is my first time using any form of birth control..." | 8.0 | November 3, 2015 | 10 | 89

Hãy tạo ra một `pandas.DataFrame` cho toàn bộ tập huấn luyện bằng cách chọn tất cả các phần tử trong `drug_dataset["train"]`:

    train_df = drug_dataset["train"][:]

🚨 Bên dưới `Dataset.set_format()` thay đổi định dạng trả về cho phương thức `__getitem __()` của tập dữ liệu. Điều này có nghĩa là khi chúng ta muốn tạo một đối tượng mới như `train_df` từ `Dataset` ở định dạng `"pandas"`, chúng ta cần cắt toàn bộ tập dữ liệu để có được một `pandas.DataFrame`. Bạn có thể tự xác minh xem kiểu dữ liệu của `drug_dataset["train"]` có phải là `Dataset`, bất kể định dạng đầu ra là gì.

Từ đây, ta có thể sử dụng tất cả các chức năng của Pandas mà ta muốn. Ví dụ, chúng ta có thể thực hiện chuỗi lạ mắt để tính toán phân phối lớp giữa các `condition`:

    frequencies = (
        train_df["condition"]
        .value_counts()
        .to_frame()
        .reset_index()
        .rename(columns={"index": "condition", "condition": "frequency"})
    )
    frequencies.head()

| condition | frequency
--- | --- | ---
0 | birth control | 27655
1 | depression | 8023
2 | acne | 5209
3 | anxiety | 4991
4 | pain | 4744

Và khi chúng ta hoàn thành phân tích Pandas của mình, chúng ta luôn có thể tạo một đối tượng `Dataset` mới bằng cách sử dụng hàm `Dataset.from_pandas()` như sau:

    from datasets import Dataset

    freq_dataset = Dataset.from_pandas(frequencies)
    freq_dataset

    Dataset({
        features: ['condition', 'frequency'],
        num_rows: 819
    })

✏️ Thử nghiệm thôi! Tính xếp hạng trung bình cho mỗi loại thuốc và lưu trữ kết quả ở dạng `Dataset` mới.

Phần này kết thúc chuyến tham quan của chúng ta về các kỹ thuật tiền xử lý khác nhau có sẵn trong 🤗 Datasets. Để hoàn thiện phần này, hãy tạo một tệp kiểm định để chuẩn bị tập dữ liệu cho việc huấn luyện một trình phân loại. Trước khi làm như vậy, chúng ta sẽ đặt lại định dạng đầu ra của `drug_dataset` từ `"pandas"` thành `"arrow"`:

    drug_dataset.reset_format()

##
Tạo ra một tệp kiểm định

Mặc dù chúng ta có một bộ dữ liệu kiểm thử có thể sử dụng để đánh giá, nhưng bạn nên giữ nguyên bộ kiểm thử và tạo một bộ kiểm định riêng trong quá trình phát triển. Khi bạn hài lòng với hiệu suất của các mô hình của mình trên bộ kiểm định, bạn có thể thực hiện kiểm tra lần cuối đối với bộ kiểm thử. Quy trình này giúp giảm thiểu rủi ro rằng bạn sẽ trang bị quá mức cho bộ kiểm thử và triển khai một mô hình không thành công trên dữ liệu trong thế giới thực.

🤗 Datasets cung cấp một hàm `Dataset.train_test_split()` dựa trên tính năng nổi tiếng từ `scikit-learn`. Hãy cùng dùng nó để chia tập huấn luyện thành các tập `train` và `validation` (ta đặt tham số `seed` cho mục đính tái tạo):

    drug_dataset_clean = drug_dataset["train"].train_test_split(train_size=0.8, seed=42)
    # Thay đổi tên mặc định "test" thành "validation"
    drug_dataset_clean["validation"] = drug_dataset_clean.pop("test")
    # Thêm "test" vào `DatasetDict`
    drug_dataset_clean["test"] = drug_dataset["test"]
    drug_dataset_clean

    DatasetDict({
        train: Dataset({
            features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length', 'review_clean'],
            num_rows: 110811
        })
        validation: Dataset({
            features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length', 'review_clean'],
            num_rows: 27703
        })
        test: Dataset({
            features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length', 'review_clean'],
            num_rows: 46108
        })
    })

Tuyệt vời, ta hiện đã chuẩn bị một tập dữ liệu sẵn sàng để huấn luyện một số mô hình! Trong phần 5, chúng tôi sẽ chỉ cho bạn cách tải tập dữ liệu lên Hugging Face Hub, nhưng bây giờ hãy quen với phân tích của chúng tôi bằng cách xem xét một số cách bạn có thể lưu tập dữ liệu trên máy cục bộ của mình.

##
Lưu một bộ dữ liệu

Mặc dù 🤗 Datasets sẽ lưu vào bộ nhớ cache mọi tập dữ liệu đã tải xuống và các hoạt động được thực hiện trên nó, nhưng đôi khi bạn sẽ muốn lưu tập dữ liệu vào đĩa (ví dụ: trong trường hợp bộ nhớ cache bị xóa). Như thể hiện trong bảng bên dưới, 🤗 Datasets cung cấp ba chức năng chính để lưu tập dữ liệu của bạn ở các định dạng khác nhau:

Định dạng dữ liệu | Hàm
--- | ---
Arrow | `Dataset.save_to_disk()`
CSV | `Dataset.to_csv()`
JSON | `Dataset.to_json()`

Ví dụ, hãy cùng lưu dữ liệu sạch của chúng ta về định dạng Arrow:

    drug_dataset_clean.save_to_disk("drug-reviews")

Nó sẽ tạo ra một kho lưu trữ với cấu trúc như sau:

    drug-reviews/
    ├── dataset_dict.json
    ├── test
    │   ├── dataset.arrow
    │   ├── dataset_info.json
    │   └── state.json
    ├── train
    │   ├── dataset.arrow
    │   ├── dataset_info.json
    │   ├── indices.arrow
    │   └── state.json
    └── validation
        ├── dataset.arrow
        ├── dataset_info.json
        ├── indices.arrow
        └── state.json

nơi chúng ta có thể thấy rằng mỗi phần tách ra được liên kết với bảng dataset.arrow của riêng nó và một số siêu dữ liệu trong dataset_info.json và state.json. Bạn có thể coi định dạng Arrow như một bảng gồm các cột và hàng ưa thích được tối ưu hóa để xây dựng các ứng dụng hiệu suất cao xử lý và vận chuyển các tập dữ liệu lớn.

Sau khi tập dữ liệu được lưu, chúng ta có thể tải nó bằng cách sử dụng hàm `load_from_disk()` như sau:

    from datasets import load_from_disk

    drug_dataset_reloaded = load_from_disk("drug-reviews")
    drug_dataset_reloaded

    DatasetDict({
        train: Dataset({
            features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length'],
            num_rows: 110811
        })
        validation: Dataset({
            features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length'],
            num_rows: 27703
        })
        test: Dataset({
            features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length'],
            num_rows: 46108
        })
    })

Đối với định dạng CSV và JSON, chúng ta phải lưu trữ từng phần thành một tệp riêng biệt. Một cách để làm điều này là lặp lại các khóa và giá trị trong đối tượng `DatasetDict`:

    for split, dataset in drug_dataset_clean.items():
        dataset.to_json(f"drug-reviews-{split}.jsonl")

Nó sẽ lưu mỗi phần dữ liệu vàođịnh dạng JSON Lines, nơi mỗi dòng trong bộ dữ liệu được lưu trữ trên một dòng JSON. Đây là một ví dụ về hình hài cua nó:

    !head -n 1 drug-reviews-train.jsonl

    {"patient_id":141780,"drugName":"Escitalopram","condition":"depression","review":"\"I seemed to experience the regular side effects of LEXAPRO, insomnia, low sex drive, sleepiness during the day. I am taking it at night because my doctor said if it made me tired to take it at night. I assumed it would and started out taking it at night. Strange dreams, some pleasant. I was diagnosed with fibromyalgia. Seems to be helping with the pain. Have had anxiety and depression in my family, and have tried quite a few other medications that haven't worked. Only have been on it for two weeks but feel more positive in my mind, want to accomplish more in my life. Hopefully the side effects will dwindle away, worth it to stick with it from hearing others responses. Great medication.\"","rating":9.0,"date":"May 29, 2011","usefulCount":10,"review_length":125}

Chúng ta sau đó có thể sử dụng các kỹ thuật trong phần 2 để tải tệp JSON như sau:

    data_files = {
        "train": "drug-reviews-train.jsonl",
        "validation": "drug-reviews-validation.jsonl",
        "test": "drug-reviews-test.jsonl",
    }
    drug_dataset_reloaded = load_dataset("json", data_files=data_files)

Và đó là nó cho chuyến du ngoạn của chúng ta với sắp xếp dữ liệu sử dụng 🤗 Datasets! Giờ ta đã có một tập dữ liệu đã được làm sạch để huấn luyện mô hình, đây là một vài ý tưởng mà bạn có thể thử:

1. Sử dụng các kỹ thuật từ Chương 3 để huấn luyện một bộ phân loại có thể dự đoán tình trạng bệnh nhân dựa trên các phản hồi về thuốc.
2. Sử dụng pipeline `summarization` từ Chương 1 để tạo các bản tóm tắt các bài đánh giá.

Tiếp theo, chúng ta sẽ xem xét cách 🤗 Datasets có thể cho phép bạn làm việc với những tập dữ liệu khổng lồ mà không làm hỏng máy tính xách tay của bạn!