fine 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +20 -10
  3. data/docs/examples/image-classification-shapes.md +83 -0
  4. data/docs/examples/text-embeddings-faq.md +98 -0
  5. data/docs/quickstart.md +209 -0
  6. data/docs/tutorials/lora-tool-calling.md +306 -0
  7. data/examples/data/generate_tool_data.rb +261 -0
  8. data/examples/data/ollama_tool_calls.jsonl +40 -0
  9. data/examples/data/sentiment_reviews.jsonl +30 -0
  10. data/examples/data/shapes/circle/circle_1.jpg +0 -0
  11. data/examples/data/shapes/circle/circle_10.jpg +0 -0
  12. data/examples/data/shapes/circle/circle_2.jpg +0 -0
  13. data/examples/data/shapes/circle/circle_3.jpg +0 -0
  14. data/examples/data/shapes/circle/circle_4.jpg +0 -0
  15. data/examples/data/shapes/circle/circle_5.jpg +0 -0
  16. data/examples/data/shapes/circle/circle_6.jpg +0 -0
  17. data/examples/data/shapes/circle/circle_7.jpg +0 -0
  18. data/examples/data/shapes/circle/circle_8.jpg +0 -0
  19. data/examples/data/shapes/circle/circle_9.jpg +0 -0
  20. data/examples/data/shapes/square/square_1.jpg +0 -0
  21. data/examples/data/shapes/square/square_10.jpg +0 -0
  22. data/examples/data/shapes/square/square_2.jpg +0 -0
  23. data/examples/data/shapes/square/square_3.jpg +0 -0
  24. data/examples/data/shapes/square/square_4.jpg +0 -0
  25. data/examples/data/shapes/square/square_5.jpg +0 -0
  26. data/examples/data/shapes/square/square_6.jpg +0 -0
  27. data/examples/data/shapes/square/square_7.jpg +0 -0
  28. data/examples/data/shapes/square/square_8.jpg +0 -0
  29. data/examples/data/shapes/square/square_9.jpg +0 -0
  30. data/examples/data/shapes/triangle/triangle_1.jpg +0 -0
  31. data/examples/data/shapes/triangle/triangle_10.jpg +0 -0
  32. data/examples/data/shapes/triangle/triangle_2.jpg +0 -0
  33. data/examples/data/shapes/triangle/triangle_3.jpg +0 -0
  34. data/examples/data/shapes/triangle/triangle_4.jpg +0 -0
  35. data/examples/data/shapes/triangle/triangle_5.jpg +0 -0
  36. data/examples/data/shapes/triangle/triangle_6.jpg +0 -0
  37. data/examples/data/shapes/triangle/triangle_7.jpg +0 -0
  38. data/examples/data/shapes/triangle/triangle_8.jpg +0 -0
  39. data/examples/data/shapes/triangle/triangle_9.jpg +0 -0
  40. data/examples/data/support_faq_pairs.jsonl +30 -0
  41. data/examples/generate_shape_images.rb +94 -0
  42. data/examples/sentiment_classification.rb +87 -0
  43. data/examples/shape_classification.rb +87 -0
  44. data/examples/support_faq_embeddings.rb +105 -0
  45. data/examples/train_lora_tools.rb +218 -0
  46. data/lib/fine/configuration.rb +173 -15
  47. data/lib/fine/datasets/image_dataset.rb +14 -2
  48. data/lib/fine/datasets/instruction_dataset.rb +17 -2
  49. data/lib/fine/datasets/text_dataset.rb +15 -5
  50. data/lib/fine/hub/config_loader.rb +4 -4
  51. data/lib/fine/hub/safetensors_loader.rb +3 -2
  52. data/lib/fine/llm.rb +39 -10
  53. data/lib/fine/lora.rb +214 -0
  54. data/lib/fine/models/bert_encoder.rb +15 -6
  55. data/lib/fine/models/bert_for_sequence_classification.rb +35 -4
  56. data/lib/fine/models/causal_lm.rb +46 -5
  57. data/lib/fine/models/gemma3_decoder.rb +25 -6
  58. data/lib/fine/models/llama_decoder.rb +9 -8
  59. data/lib/fine/models/sentence_transformer.rb +1 -1
  60. data/lib/fine/tokenizers/auto_tokenizer.rb +15 -0
  61. data/lib/fine/training/text_trainer.rb +3 -1
  62. data/lib/fine/validators.rb +304 -0
  63. data/lib/fine/version.rb +1 -1
  64. data/lib/fine.rb +4 -0
  65. metadata +47 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a12cf37fd90bb1744c5d4a91863406a7bd02742df9596113dce12e847874e969
4
- data.tar.gz: 7fba7fea8eef802257b8f20949a78befe21baad74757ab28426c147c508d4009
3
+ metadata.gz: e4f1a43c08a5a1f84d56676c0f8831cab888481c7011bee1d81ef09cbf3ce239
4
+ data.tar.gz: 0330fc12a8a1cbd25a58e86a733250b14d1f5372d194f4b86f80f1c8c073a266
5
5
  SHA512:
6
- metadata.gz: 7a6383340fc114a158e34bc905dddc24865b0ace2d4fa771abd129ca0a5bb9cbeb372682b181329ad33daffa447facf426a65cd275580f59951c4e22791e0747
7
- data.tar.gz: e8ae855256a5cd1fd2d3467177f3990db3081ff416e36931968f75b1f2578fa979fe7699d38d288fcbd35d3ab7bfa3cb545dfbee697f4b02e1a113375307f1a6
6
+ metadata.gz: c4fa0df707889ed969d75b1315ef3b98013015acbb4954cfaf78027506c104c17ed1f558871a3524e2327b1031f2d3768bc94797875934fd24418d80046fd079
7
+ data.tar.gz: 0b5f0ad3c7952b39f15f6fa3f8fed858d9b31b26e1fbeceb2afcf9697803b10c236e5257fdef855b30c15b90dc053c0aacbb36a62adbf8762c771d11f074cc74
data/README.md CHANGED
@@ -90,19 +90,27 @@ similarity = cosine_similarity(embedding1, embedding2)
90
90
 
91
91
  ---
92
92
 
93
- ### LLMs
93
+ ### LLMs (with LoRA)
94
94
 
95
- Fine-tune Gemma, Llama, Qwen and other open models for custom tasks.
95
+ Fine-tune Gemma, Llama, Qwen and other open models using LoRA—train only 0.5% of parameters.
96
96
 
97
97
  ```ruby
98
- llm = Fine::LLM.new("meta-llama/Llama-3.2-1B")
99
- llm.fit(train_file: "instructions.jsonl", epochs: 3)
98
+ model = Fine::Models::CausalLM.from_pretrained("google/gemma-3-1b-it")
100
99
 
101
- llm.generate("Explain Ruby blocks")
102
- # => "A Ruby block is a chunk of code that can be passed to a method..."
100
+ # Apply LoRA - only 0.5% of params trainable
101
+ Fine::LoRA.apply(model, rank: 32, alpha: 64)
102
+ # Trainable params: 5.96M (0.46%)
103
+
104
+ # Train on your data
105
+ trainer = Fine::Training::LLMTrainer.new(model, config, train_dataset: dataset)
106
+ trainer.fit
107
+
108
+ # Merge weights and save
109
+ Fine::LoRA.merge!(model)
110
+ model.save("my_model")
103
111
  ```
104
112
 
105
- [Full tutorial: LLM Fine-tuning](docs/tutorials/llm-fine-tuning.md)
113
+ [Full tutorial: LLM Fine-tuning](docs/tutorials/llm-fine-tuning.md) | [LoRA Tool Calling](docs/tutorials/lora-tool-calling.md)
106
114
 
107
115
  ---
108
116
 
@@ -114,7 +122,7 @@ gem 'fine'
114
122
 
115
123
  Requires Ruby 3.1+, LibTorch, and libvips.
116
124
 
117
- [Full installation guide](docs/installation.md)
125
+ [Full installation guide](docs/installation.md) | [Quickstart](docs/quickstart.md)
118
126
 
119
127
  **Quick setup (macOS):**
120
128
  ```bash
@@ -155,8 +163,9 @@ bundle install
155
163
 
156
164
  | Model | Parameters | Best For |
157
165
  |-------|------------|----------|
166
+ | `google/gemma-3-1b-it` | 1B | Fast experiments, tool calling |
158
167
  | `meta-llama/Llama-3.2-1B` | 1B | Fast experiments |
159
- | `google/gemma-2b` | 2B | Good balance |
168
+ | `google/gemma-3-4b-it` | 4B | Good balance |
160
169
  | `Qwen/Qwen2-1.5B` | 1.5B | Multilingual |
161
170
  | `mistralai/Mistral-7B-v0.1` | 7B | Best quality |
162
171
 
@@ -201,7 +210,8 @@ llm.export_gguf("model.gguf", quantization: :q4_0)
201
210
  - [x] Text embedding models
202
211
  - [x] LLM fine-tuning (Gemma, Llama, Qwen)
203
212
  - [x] ONNX & GGUF export
204
- - [ ] LoRA/QLoRA fine-tuning
213
+ - [x] LoRA fine-tuning
214
+ - [ ] QLoRA (4-bit quantized LoRA)
205
215
 
206
216
  ## Contributing
207
217
 
@@ -0,0 +1,83 @@
1
+ # Image Classification: Shape Recognition
2
+
3
+ This example demonstrates fine-tuning SigLIP2 to classify images by dominant color patterns.
4
+
5
+ ## Setup
6
+
7
+ ```ruby
8
+ require "fine"
9
+
10
+ Fine.configure { |c| c.progress_bar = false }
11
+ ```
12
+
13
+ ## Generate Training Data
14
+
15
+ Create synthetic training images with different colors representing different "shapes":
16
+ - **Circles**: Red-ish colors (RGB around 220, 80, 80)
17
+ - **Squares**: Green-ish colors (RGB around 80, 180, 80)
18
+ - **Triangles**: Blue-ish colors (RGB around 80, 80, 220)
19
+
20
+ ```ruby
21
+ # Run: ruby examples/generate_shape_images.rb
22
+ # Creates 30 images (10 per class) in examples/data/shapes/
23
+ ```
24
+
25
+ ## Train the Classifier
26
+
27
+ ```ruby
28
+ classifier = Fine::ImageClassifier.new("google/siglip2-base-patch16-224") do |config|
29
+ config.epochs = 5
30
+ config.batch_size = 4
31
+ config.learning_rate = 1e-4
32
+ config.freeze_encoder = false # Fine-tune entire model
33
+
34
+ config.on_epoch_end do |epoch, metrics|
35
+ puts "Epoch #{epoch}: loss=#{metrics[:loss].round(4)}"
36
+ end
37
+ end
38
+
39
+ history = classifier.fit(train_dir: "examples/data/shapes", epochs: 5)
40
+ ```
41
+
42
+ ## Training Results
43
+
44
+ ```
45
+ Epoch 0: loss=0.8432
46
+ Epoch 1: loss=0.1725
47
+ Epoch 2: loss=0.0321
48
+ Epoch 3: loss=0.0027
49
+ Epoch 4: loss=0.0006
50
+ ```
51
+
52
+ The loss dropped from 0.84 to 0.0006 - a 99.9% improvement!
53
+
54
+ ## Test Predictions
55
+
56
+ ```ruby
57
+ # All predictions are 100% confident and correct
58
+ classifier.predict("data/shapes/circle/circle_1.jpg")
59
+ # => [{ label: "circle", score: 1.0 }]
60
+
61
+ classifier.predict("data/shapes/square/square_1.jpg")
62
+ # => [{ label: "square", score: 1.0 }]
63
+
64
+ classifier.predict("data/shapes/triangle/triangle_1.jpg")
65
+ # => [{ label: "triangle", score: 0.999 }]
66
+ ```
67
+
68
+ ## Save and Load
69
+
70
+ ```ruby
71
+ # Save
72
+ classifier.save("/tmp/shape-classifier")
73
+
74
+ # Load later
75
+ loaded = Fine::ImageClassifier.load("/tmp/shape-classifier")
76
+ loaded.predict("new_image.jpg")
77
+ ```
78
+
79
+ ## Key Takeaways
80
+
81
+ - SigLIP2 quickly learns visual patterns even with small datasets (30 images)
82
+ - Fine-tuning the full model (`freeze_encoder: false`) achieves best results
83
+ - The model achieves perfect accuracy after just 5 epochs
@@ -0,0 +1,98 @@
1
+ # Text Embeddings: Customer Support FAQ Matching
2
+
3
+ This example demonstrates fine-tuning a sentence transformer for semantic search in a customer support context.
4
+
5
+ ## Setup
6
+
7
+ ```ruby
8
+ require "fine"
9
+
10
+ Fine.configure { |c| c.progress_bar = false }
11
+ ```
12
+
13
+ ## Training Data Format
14
+
15
+ Create query-answer pairs in JSONL format:
16
+
17
+ ```jsonl
18
+ {"query": "How do I reset my password?", "positive": "To reset your password, click 'Forgot Password' on the login page."}
19
+ {"query": "I forgot my login credentials", "positive": "To reset your password, click 'Forgot Password' on the login page."}
20
+ {"query": "How long does shipping take?", "positive": "Standard shipping takes 3-5 business days."}
21
+ ```
22
+
23
+ Multiple queries can map to the same answer to teach semantic similarity.
24
+
25
+ ## Train the Embedder
26
+
27
+ ```ruby
28
+ embedder = Fine::TextEmbedder.new("sentence-transformers/all-MiniLM-L6-v2") do |config|
29
+ config.epochs = 2
30
+ config.batch_size = 8
31
+ config.learning_rate = 2e-5
32
+ end
33
+
34
+ # Test pre-training similarity
35
+ pre_sim = embedder.similarity("How can I get my money back?", "To initiate a return, go to your orders...")
36
+ puts "Pre-training: #{pre_sim.round(4)}" # => 0.4008
37
+
38
+ # Fine-tune
39
+ history = embedder.fit(train_file: "data/support_faq_pairs.jsonl")
40
+
41
+ # Test post-training
42
+ post_sim = embedder.similarity("How can I get my money back?", "To initiate a return, go to your orders...")
43
+ puts "Post-training: #{post_sim.round(4)}" # => 0.4723
44
+ ```
45
+
46
+ ## Training Results
47
+
48
+ ```
49
+ Epoch 0: loss=4.3761
50
+ Epoch 1: loss=5.8889
51
+
52
+ Post-training similarity: 0.4723
53
+ Improvement: 7.15 percentage points
54
+ ```
55
+
56
+ ## Semantic Search
57
+
58
+ ```ruby
59
+ faq_corpus = [
60
+ "To reset your password, click 'Forgot Password' on the login page.",
61
+ "Standard shipping takes 3-5 business days.",
62
+ "To initiate a return, go to your order history and click 'Request Return'.",
63
+ "We accept Visa, Mastercard, American Express, PayPal, and Apple Pay.",
64
+ "Yes, we ship to over 50 countries.",
65
+ "You can reach us via live chat, email, or phone at 1-800-555-0123."
66
+ ]
67
+
68
+ # Search for relevant FAQ
69
+ results = embedder.search("I need to get my money back", faq_corpus, top_k: 2)
70
+ # => [
71
+ # { text: "To initiate a return...", score: 0.548 },
72
+ # { text: "You can reach us via...", score: 0.425 }
73
+ # ]
74
+
75
+ results = embedder.search("What's the phone number for help?", faq_corpus)
76
+ # => [{ text: "You can reach us via...", score: 0.461 }]
77
+
78
+ results = embedder.search("Can you deliver to Germany?", faq_corpus)
79
+ # => [{ text: "Yes, we ship to over 50 countries...", score: 0.515 }]
80
+ ```
81
+
82
+ ## Save and Load
83
+
84
+ ```ruby
85
+ # Save
86
+ embedder.save("/tmp/support-faq-embedder")
87
+
88
+ # Load later
89
+ loaded = Fine::TextEmbedder.load("/tmp/support-faq-embedder")
90
+ loaded.search("your query", corpus)
91
+ ```
92
+
93
+ ## Key Takeaways
94
+
95
+ - Even 2 epochs of fine-tuning improves similarity for domain-specific queries
96
+ - Multiple Negatives Ranking Loss learns to distinguish relevant from irrelevant answers
97
+ - The model correctly identifies the best FAQ match for natural language queries
98
+ - Pre-trained models already provide good baseline (0.4 similarity), fine-tuning improves it
@@ -0,0 +1,209 @@
1
+ # Quickstart
2
+
3
+ Get started with Fine in under 5 minutes.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # macOS
9
+ brew install pytorch libvips
10
+ gem install fine
11
+
12
+ # Or add to Gemfile
13
+ gem 'fine'
14
+ ```
15
+
16
+ ## Text Classification
17
+
18
+ Classify text into categories (sentiment, spam, intent).
19
+
20
+ **1. Prepare your data** (`reviews.jsonl`):
21
+ ```json
22
+ {"text": "This product is amazing!", "label": "positive"}
23
+ {"text": "Terrible experience, waste of money", "label": "negative"}
24
+ {"text": "It's okay, nothing special", "label": "neutral"}
25
+ ```
26
+
27
+ **2. Train and use:**
28
+ ```ruby
29
+ require 'fine'
30
+
31
+ classifier = Fine::TextClassifier.new("distilbert-base-uncased")
32
+ classifier.fit(train_file: "reviews.jsonl", epochs: 3)
33
+
34
+ classifier.predict("Best purchase ever!")
35
+ # => [{ label: "positive", score: 0.95 }]
36
+
37
+ classifier.save("my_classifier")
38
+ ```
39
+
40
+ ---
41
+
42
+ ## Image Classification
43
+
44
+ Classify images into categories.
45
+
46
+ **1. Organize your images:**
47
+ ```
48
+ data/
49
+ cats/
50
+ cat1.jpg
51
+ cat2.jpg
52
+ dogs/
53
+ dog1.jpg
54
+ dog2.jpg
55
+ ```
56
+
57
+ **2. Train and use:**
58
+ ```ruby
59
+ require 'fine'
60
+
61
+ classifier = Fine::ImageClassifier.new("google/siglip2-base-patch16-224")
62
+ classifier.fit(train_dir: "data/", epochs: 3)
63
+
64
+ classifier.predict("test_image.jpg")
65
+ # => [{ label: "cat", score: 0.92 }]
66
+
67
+ classifier.save("my_image_classifier")
68
+ ```
69
+
70
+ ---
71
+
72
+ ## Text Embeddings
73
+
74
+ Generate embeddings for semantic search.
75
+
76
+ **1. Prepare training pairs** (`pairs.jsonl`):
77
+ ```json
78
+ {"query": "How do I reset my password?", "positive": "Click 'Forgot Password' on the login page"}
79
+ {"query": "What are your hours?", "positive": "We're open Monday-Friday, 9am-5pm"}
80
+ ```
81
+
82
+ **2. Train and use:**
83
+ ```ruby
84
+ require 'fine'
85
+
86
+ embedder = Fine::TextEmbedder.new("sentence-transformers/all-MiniLM-L6-v2")
87
+ embedder.fit(train_file: "pairs.jsonl", epochs: 3)
88
+
89
+ # Get embeddings
90
+ embedding = embedder.encode("How do I change my password?")
91
+
92
+ # Semantic search
93
+ results = embedder.search("password help", corpus, top_k: 5)
94
+
95
+ embedder.save("my_embedder")
96
+ ```
97
+
98
+ ---
99
+
100
+ ## LLM Fine-tuning (with LoRA)
101
+
102
+ Fine-tune language models using LoRA—only 0.5% of parameters are trainable.
103
+
104
+ **1. Prepare instruction data** (`instructions.jsonl`):
105
+ ```json
106
+ {"instruction": "Summarize this text", "input": "Long article here...", "output": "Brief summary"}
107
+ {"instruction": "Translate to French", "input": "Hello world", "output": "Bonjour le monde"}
108
+ ```
109
+
110
+ **2. Train with LoRA:**
111
+ ```ruby
112
+ require 'fine'
113
+
114
+ # Load model and apply LoRA
115
+ model = Fine::Models::CausalLM.from_pretrained("google/gemma-3-1b-it")
116
+ Fine::LoRA.apply(model, rank: 32, alpha: 64)
117
+ # => "Trainable params: 5.96M (0.46%)"
118
+
119
+ # Load dataset
120
+ tokenizer = Fine::Tokenizers::AutoTokenizer.from_pretrained("google/gemma-3-1b-it")
121
+ dataset = Fine::Datasets::InstructionDataset.from_jsonl("instructions.jsonl", tokenizer: tokenizer)
122
+
123
+ # Train
124
+ config = Fine::LLMConfiguration.new
125
+ trainer = Fine::Training::LLMTrainer.new(model, config, train_dataset: dataset)
126
+ trainer.fit
127
+
128
+ # Merge LoRA weights and save
129
+ Fine::LoRA.merge!(model)
130
+ model.save("my_llm")
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Data Formats Reference
136
+
137
+ ### Text Classification
138
+ ```json
139
+ {"text": "Your text here", "label": "category_name"}
140
+ ```
141
+
142
+ ### Text Pairs (Embeddings)
143
+ ```json
144
+ {"query": "Question text", "positive": "Matching answer"}
145
+ ```
146
+ Alternative field names: `text_a`/`text_b`, `anchor`/`positive`, `sentence1`/`sentence2`
147
+
148
+ ### Instructions (LLM)
149
+
150
+ **Alpaca format:**
151
+ ```json
152
+ {"instruction": "Task description", "input": "Optional context", "output": "Expected response"}
153
+ ```
154
+
155
+ **ShareGPT format:**
156
+ ```json
157
+ {"conversations": [{"from": "human", "value": "Hi"}, {"from": "assistant", "value": "Hello!"}]}
158
+ ```
159
+
160
+ **Simple format:**
161
+ ```json
162
+ {"prompt": "Input text", "completion": "Output text"}
163
+ ```
164
+
165
+ ---
166
+
167
+ ## Configuration
168
+
169
+ All classifiers accept a configuration block:
170
+
171
+ ```ruby
172
+ Fine::TextClassifier.new("distilbert-base-uncased") do |config|
173
+ config.epochs = 5
174
+ config.batch_size = 16
175
+ config.learning_rate = 2e-5
176
+
177
+ config.on_epoch_end do |epoch, metrics|
178
+ puts "Epoch #{epoch}: loss=#{metrics[:loss]}"
179
+ end
180
+ end
181
+ ```
182
+
183
+ Common options:
184
+ - `epochs` - Number of training passes (default: 3)
185
+ - `batch_size` - Samples per batch (default: 8-16)
186
+ - `learning_rate` - Learning rate (default: 2e-5)
187
+ - `max_length` - Max sequence length (default: 128-2048)
188
+
189
+ ---
190
+
191
+ ## Export for Production
192
+
193
+ ```ruby
194
+ # ONNX (for ONNX Runtime, TensorRT)
195
+ classifier.export_onnx("model.onnx")
196
+
197
+ # GGUF (for llama.cpp, Ollama)
198
+ llm.export_gguf("model.gguf", quantization: :q4_0)
199
+ ```
200
+
201
+ ---
202
+
203
+ ## Next Steps
204
+
205
+ - [Text Classification Tutorial](tutorials/text-classification.md)
206
+ - [Image Classification Tutorial](tutorials/siglip2-image-classification.md)
207
+ - [LLM Fine-tuning Tutorial](tutorials/llm-fine-tuning.md)
208
+ - [LoRA Tool Calling](tutorials/lora-tool-calling.md)
209
+ - [Model Export](tutorials/model-export.md)