fine 0.1.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 (69) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG.md +38 -0
  4. data/Gemfile +6 -0
  5. data/Gemfile.lock +167 -0
  6. data/LICENSE +21 -0
  7. data/README.md +212 -0
  8. data/Rakefile +6 -0
  9. data/docs/installation.md +151 -0
  10. data/docs/tutorials/llm-fine-tuning.md +246 -0
  11. data/docs/tutorials/model-export.md +200 -0
  12. data/docs/tutorials/siglip2-image-classification.md +130 -0
  13. data/docs/tutorials/siglip2-object-recognition.md +203 -0
  14. data/docs/tutorials/siglip2-similarity-search.md +152 -0
  15. data/docs/tutorials/text-classification.md +233 -0
  16. data/docs/tutorials/text-embeddings.md +211 -0
  17. data/examples/basic_classification.rb +70 -0
  18. data/examples/data/tool_calls.jsonl +30 -0
  19. data/examples/demo_training.rb +78 -0
  20. data/examples/finetune_gemma3_tools.rb +135 -0
  21. data/examples/real_llm_test.rb +128 -0
  22. data/examples/real_text_classification_test.rb +90 -0
  23. data/examples/real_text_embedder_test.rb +110 -0
  24. data/examples/real_training_test.rb +88 -0
  25. data/examples/test_export.rb +28 -0
  26. data/examples/test_image_classifier.rb +79 -0
  27. data/examples/test_llm.rb +100 -0
  28. data/examples/test_text_classifier.rb +59 -0
  29. data/lib/fine/callbacks/base.rb +140 -0
  30. data/lib/fine/callbacks/progress_bar.rb +66 -0
  31. data/lib/fine/configuration.rb +106 -0
  32. data/lib/fine/datasets/data_loader.rb +63 -0
  33. data/lib/fine/datasets/image_dataset.rb +203 -0
  34. data/lib/fine/datasets/instruction_dataset.rb +226 -0
  35. data/lib/fine/datasets/text_data_loader.rb +88 -0
  36. data/lib/fine/datasets/text_dataset.rb +266 -0
  37. data/lib/fine/error.rb +49 -0
  38. data/lib/fine/export/gguf_exporter.rb +424 -0
  39. data/lib/fine/export/onnx_exporter.rb +249 -0
  40. data/lib/fine/export.rb +53 -0
  41. data/lib/fine/hub/config_loader.rb +145 -0
  42. data/lib/fine/hub/model_downloader.rb +136 -0
  43. data/lib/fine/hub/safetensors_loader.rb +108 -0
  44. data/lib/fine/image_classifier.rb +256 -0
  45. data/lib/fine/llm.rb +336 -0
  46. data/lib/fine/models/base.rb +48 -0
  47. data/lib/fine/models/bert_encoder.rb +202 -0
  48. data/lib/fine/models/bert_for_sequence_classification.rb +226 -0
  49. data/lib/fine/models/causal_lm.rb +279 -0
  50. data/lib/fine/models/classification_head.rb +24 -0
  51. data/lib/fine/models/gemma3_decoder.rb +244 -0
  52. data/lib/fine/models/llama_decoder.rb +297 -0
  53. data/lib/fine/models/sentence_transformer.rb +202 -0
  54. data/lib/fine/models/siglip2_for_image_classification.rb +155 -0
  55. data/lib/fine/models/siglip2_vision_encoder.rb +190 -0
  56. data/lib/fine/text_classifier.rb +250 -0
  57. data/lib/fine/text_embedder.rb +221 -0
  58. data/lib/fine/tokenizers/auto_tokenizer.rb +208 -0
  59. data/lib/fine/training/llm_trainer.rb +212 -0
  60. data/lib/fine/training/text_trainer.rb +275 -0
  61. data/lib/fine/training/trainer.rb +194 -0
  62. data/lib/fine/transforms/compose.rb +28 -0
  63. data/lib/fine/transforms/normalize.rb +33 -0
  64. data/lib/fine/transforms/resize.rb +35 -0
  65. data/lib/fine/transforms/to_tensor.rb +53 -0
  66. data/lib/fine/version.rb +3 -0
  67. data/lib/fine.rb +112 -0
  68. data/mise.toml +2 -0
  69. metadata +240 -0
@@ -0,0 +1,211 @@
1
+ # Fine-tuning Text Embedding Models
2
+
3
+ Train text embeddings for semantic search, clustering, and similarity matching.
4
+
5
+ ## When to Use This
6
+
7
+ - Build semantic search for your domain (legal docs, medical records, code)
8
+ - Improve retrieval for RAG applications
9
+ - Cluster similar documents
10
+ - Match queries to FAQ answers
11
+ - Detect duplicate content
12
+
13
+ ## Supported Models
14
+
15
+ | Model | Size | Use Case |
16
+ |-------|------|----------|
17
+ | `sentence-transformers/all-MiniLM-L6-v2` | 22M | Fast, general purpose |
18
+ | `sentence-transformers/all-mpnet-base-v2` | 110M | Better quality |
19
+ | `BAAI/bge-small-en-v1.5` | 33M | Retrieval optimized |
20
+ | `BAAI/bge-base-en-v1.5` | 110M | Best retrieval quality |
21
+
22
+ ## Dataset Format
23
+
24
+ Prepare your data as pairs or triplets:
25
+
26
+ ### Pairs (query, positive)
27
+
28
+ ```
29
+ data/pairs.jsonl
30
+ {"query": "How do I reset my password?", "positive": "To reset your password, click 'Forgot Password' on the login page."}
31
+ {"query": "What's your return policy?", "positive": "We accept returns within 30 days of purchase."}
32
+ ```
33
+
34
+ ### Triplets (query, positive, negative)
35
+
36
+ ```
37
+ data/triplets.jsonl
38
+ {"query": "python list append", "positive": "list.append(x) adds item x to the end", "negative": "list.pop() removes the last item"}
39
+ ```
40
+
41
+ ## Basic Training
42
+
43
+ ```ruby
44
+ require "fine"
45
+
46
+ embedder = Fine::TextEmbedder.new("sentence-transformers/all-MiniLM-L6-v2")
47
+
48
+ embedder.fit(
49
+ train_file: "data/pairs.jsonl",
50
+ epochs: 3
51
+ )
52
+
53
+ embedder.save("models/my_embedder")
54
+ ```
55
+
56
+ ## Training for Semantic Search
57
+
58
+ Optimize for retrieval with contrastive loss:
59
+
60
+ ```ruby
61
+ embedder = Fine::TextEmbedder.new("BAAI/bge-base-en-v1.5") do |config|
62
+ config.epochs = 5
63
+ config.batch_size = 32
64
+ config.learning_rate = 2e-5
65
+ config.loss = :multiple_negatives_ranking # Best for retrieval
66
+
67
+ config.on_epoch_end do |epoch, metrics|
68
+ puts "Epoch #{epoch}: loss=#{metrics[:loss]}"
69
+ end
70
+ end
71
+
72
+ embedder.fit(train_file: "data/queries_documents.jsonl")
73
+ embedder.save("models/search_embedder")
74
+ ```
75
+
76
+ ## Generating Embeddings
77
+
78
+ ```ruby
79
+ embedder = Fine::TextEmbedder.load("models/my_embedder")
80
+
81
+ # Single text
82
+ embedding = embedder.encode("How do I cancel my subscription?")
83
+ # => Array of 384 floats (for MiniLM)
84
+
85
+ # Batch encoding
86
+ embeddings = embedder.encode([
87
+ "First document",
88
+ "Second document",
89
+ "Third document"
90
+ ])
91
+ # => Array of 3 embeddings
92
+ ```
93
+
94
+ ## Building a Search Index
95
+
96
+ ```ruby
97
+ class SemanticSearch
98
+ def initialize(embedder)
99
+ @embedder = embedder
100
+ @documents = []
101
+ @embeddings = []
102
+ end
103
+
104
+ def add(text, metadata = {})
105
+ @documents << { text: text, metadata: metadata }
106
+ @embeddings << @embedder.encode(text)
107
+ end
108
+
109
+ def search(query, top_k: 5)
110
+ query_emb = @embedder.encode(query)
111
+
112
+ scores = @embeddings.map { |emb| cosine_similarity(query_emb, emb) }
113
+
114
+ scores
115
+ .each_with_index
116
+ .sort_by { |score, _| -score }
117
+ .first(top_k)
118
+ .map { |score, idx| { document: @documents[idx], score: score } }
119
+ end
120
+
121
+ private
122
+
123
+ def cosine_similarity(a, b)
124
+ dot = a.zip(b).sum { |x, y| x * y }
125
+ norm_a = Math.sqrt(a.sum { |x| x * x })
126
+ norm_b = Math.sqrt(b.sum { |x| x * x })
127
+ dot / (norm_a * norm_b)
128
+ end
129
+ end
130
+
131
+ # Usage
132
+ search = SemanticSearch.new(embedder)
133
+
134
+ # Index your documents
135
+ documents.each { |doc| search.add(doc[:text], id: doc[:id]) }
136
+
137
+ # Search
138
+ results = search.search("how to get a refund")
139
+ results.each do |r|
140
+ puts "#{r[:score].round(3)}: #{r[:document][:text][0..50]}..."
141
+ end
142
+ ```
143
+
144
+ ## Domain Adaptation
145
+
146
+ Fine-tune on your specific domain for better results:
147
+
148
+ ```ruby
149
+ # Prepare domain-specific pairs
150
+ # Example: customer support queries matched to answers
151
+ pairs = [
152
+ { query: "broken screen", positive: "Screen repair costs $99 and takes 2-3 days" },
153
+ { query: "cracked display", positive: "Screen repair costs $99 and takes 2-3 days" },
154
+ # ... more domain-specific examples
155
+ ]
156
+
157
+ # Save as JSONL
158
+ File.write("data/support_pairs.jsonl", pairs.map(&:to_json).join("\n"))
159
+
160
+ # Fine-tune
161
+ embedder = Fine::TextEmbedder.new("sentence-transformers/all-mpnet-base-v2")
162
+ embedder.fit(train_file: "data/support_pairs.jsonl", epochs: 3)
163
+ embedder.save("models/support_search")
164
+ ```
165
+
166
+ ## Hard Negatives Mining
167
+
168
+ For better quality, use hard negatives (similar but wrong answers):
169
+
170
+ ```ruby
171
+ embedder = Fine::TextEmbedder.new("BAAI/bge-base-en-v1.5") do |config|
172
+ config.loss = :triplet
173
+ config.margin = 0.5 # Minimum distance between positive and negative
174
+ end
175
+
176
+ # Triplet format
177
+ embedder.fit(train_file: "data/triplets.jsonl")
178
+ ```
179
+
180
+ ## Evaluation
181
+
182
+ Test retrieval quality:
183
+
184
+ ```ruby
185
+ def evaluate_retrieval(embedder, test_queries, ground_truth)
186
+ hits_at_1 = 0
187
+ hits_at_5 = 0
188
+
189
+ test_queries.each do |query, expected_doc_id|
190
+ results = search.search(query, top_k: 5)
191
+
192
+ result_ids = results.map { |r| r[:document][:metadata][:id] }
193
+
194
+ hits_at_1 += 1 if result_ids[0] == expected_doc_id
195
+ hits_at_5 += 1 if result_ids.include?(expected_doc_id)
196
+ end
197
+
198
+ {
199
+ recall_at_1: hits_at_1.to_f / test_queries.size,
200
+ recall_at_5: hits_at_5.to_f / test_queries.size
201
+ }
202
+ end
203
+ ```
204
+
205
+ ## Best Practices
206
+
207
+ 1. **Start with a good base model** - BGE models work best for retrieval
208
+ 2. **Use domain-specific data** - Even 1000 pairs helps significantly
209
+ 3. **Include hard negatives** - Similar but incorrect matches improve precision
210
+ 4. **Evaluate on held-out queries** - Don't overfit to training data
211
+ 5. **Consider asymmetric models** - Some models use different encodings for queries vs documents
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Basic image classification example
5
+ #
6
+ # This example shows how to fine-tune a SigLIP2 model for image classification.
7
+ #
8
+ # Dataset structure expected:
9
+ # data/
10
+ # train/
11
+ # cat/
12
+ # cat1.jpg
13
+ # cat2.jpg
14
+ # dog/
15
+ # dog1.jpg
16
+ # dog2.jpg
17
+ # val/
18
+ # cat/
19
+ # cat3.jpg
20
+ # dog/
21
+ # dog3.jpg
22
+
23
+ require "fine"
24
+
25
+ # Configure Fine (optional)
26
+ Fine.configure do |config|
27
+ config.cache_dir = File.expand_path("~/.cache/fine")
28
+ config.progress_bar = true
29
+ end
30
+
31
+ # Create a classifier
32
+ classifier = Fine::ImageClassifier.new("google/siglip2-base-patch16-224") do |config|
33
+ # Training settings
34
+ config.epochs = 3
35
+ config.batch_size = 8
36
+ config.learning_rate = 2e-4
37
+
38
+ # Model settings
39
+ config.freeze_encoder = false # Full fine-tuning
40
+ config.dropout = 0.1
41
+
42
+ # Callbacks
43
+ config.on_epoch_end do |epoch, metrics|
44
+ puts "Epoch #{epoch + 1}:"
45
+ puts " Train Loss: #{metrics[:loss].round(4)}"
46
+ puts " Train Acc: #{(metrics[:accuracy] * 100).round(2)}%"
47
+ if metrics[:val_loss]
48
+ puts " Val Loss: #{metrics[:val_loss].round(4)}"
49
+ puts " Val Acc: #{(metrics[:val_accuracy] * 100).round(2)}%"
50
+ end
51
+ end
52
+ end
53
+
54
+ # Train the model
55
+ puts "Starting training..."
56
+ history = classifier.fit(
57
+ train_dir: "data/train",
58
+ val_dir: "data/val"
59
+ )
60
+
61
+ # Save the model
62
+ classifier.save("models/my_classifier")
63
+ puts "Model saved to models/my_classifier"
64
+
65
+ # Make predictions
66
+ puts "\nMaking predictions..."
67
+ predictions = classifier.predict("data/test/image.jpg")
68
+ predictions.each do |pred|
69
+ puts " #{pred[:label]}: #{(pred[:score] * 100).round(2)}%"
70
+ end
@@ -0,0 +1,30 @@
1
+ {"instruction": "What's the weather in Tokyo?", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"get_weather\", \"description\": \"Get current weather for a location\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\", \"description\": \"City name\"}}, \"required\": [\"location\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"get_weather\", \"arguments\": {\"location\": \"Tokyo\"}}]"}
2
+ {"instruction": "Calculate 25 * 4 + 10", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"calculate\", \"description\": \"Evaluate a math expression\", \"parameters\": {\"type\": \"object\", \"properties\": {\"expression\": {\"type\": \"string\", \"description\": \"Math expression\"}}, \"required\": [\"expression\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"calculate\", \"arguments\": {\"expression\": \"25 * 4 + 10\"}}]"}
3
+ {"instruction": "Search for the latest news about AI", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"search_web\", \"description\": \"Search the web\", \"parameters\": {\"type\": \"object\", \"properties\": {\"query\": {\"type\": \"string\", \"description\": \"Search query\"}}, \"required\": [\"query\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"search_web\", \"arguments\": {\"query\": \"latest news about AI\"}}]"}
4
+ {"instruction": "What's the temperature in New York City?", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"get_weather\", \"description\": \"Get current weather\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\"}}, \"required\": [\"location\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"get_weather\", \"arguments\": {\"location\": \"New York City\"}}]"}
5
+ {"instruction": "How much is 15% of 200?", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"calculate\", \"description\": \"Evaluate math\", \"parameters\": {\"type\": \"object\", \"properties\": {\"expression\": {\"type\": \"string\"}}, \"required\": [\"expression\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"calculate\", \"arguments\": {\"expression\": \"0.15 * 200\"}}]"}
6
+ {"instruction": "Find information about Ruby programming", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"search_web\", \"description\": \"Search the web\", \"parameters\": {\"type\": \"object\", \"properties\": {\"query\": {\"type\": \"string\"}}, \"required\": [\"query\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"search_web\", \"arguments\": {\"query\": \"Ruby programming language\"}}]"}
7
+ {"instruction": "Check weather in London", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"get_weather\", \"description\": \"Get weather\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\"}}, \"required\": [\"location\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"get_weather\", \"arguments\": {\"location\": \"London\"}}]"}
8
+ {"instruction": "What is sqrt(144)?", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"calculate\", \"description\": \"Evaluate math\", \"parameters\": {\"type\": \"object\", \"properties\": {\"expression\": {\"type\": \"string\"}}, \"required\": [\"expression\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"calculate\", \"arguments\": {\"expression\": \"sqrt(144)\"}}]"}
9
+ {"instruction": "Look up population of Japan", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"search_web\", \"description\": \"Search web\", \"parameters\": {\"type\": \"object\", \"properties\": {\"query\": {\"type\": \"string\"}}, \"required\": [\"query\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"search_web\", \"arguments\": {\"query\": \"population of Japan\"}}]"}
10
+ {"instruction": "Is it raining in Seattle?", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"get_weather\", \"description\": \"Get weather\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\"}}, \"required\": [\"location\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"get_weather\", \"arguments\": {\"location\": \"Seattle\"}}]"}
11
+ {"instruction": "Divide 1000 by 8", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"calculate\", \"description\": \"Math\", \"parameters\": {\"type\": \"object\", \"properties\": {\"expression\": {\"type\": \"string\"}}, \"required\": [\"expression\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"calculate\", \"arguments\": {\"expression\": \"1000 / 8\"}}]"}
12
+ {"instruction": "Search machine learning tutorials", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"search_web\", \"description\": \"Search\", \"parameters\": {\"type\": \"object\", \"properties\": {\"query\": {\"type\": \"string\"}}, \"required\": [\"query\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"search_web\", \"arguments\": {\"query\": \"machine learning tutorials\"}}]"}
13
+ {"instruction": "Weather in Paris today", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"get_weather\", \"description\": \"Weather\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\"}}, \"required\": [\"location\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"get_weather\", \"arguments\": {\"location\": \"Paris\"}}]"}
14
+ {"instruction": "2 to the power of 10", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"calculate\", \"description\": \"Math\", \"parameters\": {\"type\": \"object\", \"properties\": {\"expression\": {\"type\": \"string\"}}, \"required\": [\"expression\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"calculate\", \"arguments\": {\"expression\": \"2 ** 10\"}}]"}
15
+ {"instruction": "Send email to john@example.com about the meeting tomorrow", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"send_email\", \"description\": \"Send an email\", \"parameters\": {\"type\": \"object\", \"properties\": {\"to\": {\"type\": \"string\"}, \"subject\": {\"type\": \"string\"}, \"body\": {\"type\": \"string\"}}, \"required\": [\"to\", \"subject\", \"body\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"send_email\", \"arguments\": {\"to\": \"john@example.com\", \"subject\": \"Meeting Tomorrow\", \"body\": \"Hi, I wanted to discuss the meeting scheduled for tomorrow.\"}}]"}
16
+ {"instruction": "Create event for Friday at 3pm called Team Standup", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"create_event\", \"description\": \"Create calendar event\", \"parameters\": {\"type\": \"object\", \"properties\": {\"title\": {\"type\": \"string\"}, \"date\": {\"type\": \"string\"}, \"time\": {\"type\": \"string\"}}, \"required\": [\"title\", \"date\", \"time\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"create_event\", \"arguments\": {\"title\": \"Team Standup\", \"date\": \"Friday\", \"time\": \"3pm\"}}]"}
17
+ {"instruction": "Set reminder to buy milk", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"set_reminder\", \"description\": \"Set a reminder\", \"parameters\": {\"type\": \"object\", \"properties\": {\"message\": {\"type\": \"string\"}}, \"required\": [\"message\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"set_reminder\", \"arguments\": {\"message\": \"buy milk\"}}]"}
18
+ {"instruction": "Translate hello to Spanish", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"translate\", \"description\": \"Translate text\", \"parameters\": {\"type\": \"object\", \"properties\": {\"text\": {\"type\": \"string\"}, \"target_language\": {\"type\": \"string\"}}, \"required\": [\"text\", \"target_language\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"translate\", \"arguments\": {\"text\": \"hello\", \"target_language\": \"Spanish\"}}]"}
19
+ {"instruction": "Play jazz music", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"play_music\", \"description\": \"Play music\", \"parameters\": {\"type\": \"object\", \"properties\": {\"genre\": {\"type\": \"string\"}}, \"required\": [\"genre\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"play_music\", \"arguments\": {\"genre\": \"jazz\"}}]"}
20
+ {"instruction": "Turn off living room lights", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"control_lights\", \"description\": \"Control smart lights\", \"parameters\": {\"type\": \"object\", \"properties\": {\"room\": {\"type\": \"string\"}, \"action\": {\"type\": \"string\", \"enum\": [\"on\", \"off\"]}}, \"required\": [\"room\", \"action\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"control_lights\", \"arguments\": {\"room\": \"living room\", \"action\": \"off\"}}]"}
21
+ {"instruction": "What time is it in Sydney?", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"get_time\", \"description\": \"Get time in timezone\", \"parameters\": {\"type\": \"object\", \"properties\": {\"timezone\": {\"type\": \"string\"}}, \"required\": [\"timezone\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"get_time\", \"arguments\": {\"timezone\": \"Australia/Sydney\"}}]"}
22
+ {"instruction": "Book flight to Miami next Friday", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"book_flight\", \"description\": \"Book a flight\", \"parameters\": {\"type\": \"object\", \"properties\": {\"destination\": {\"type\": \"string\"}, \"date\": {\"type\": \"string\"}}, \"required\": [\"destination\", \"date\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"book_flight\", \"arguments\": {\"destination\": \"Miami\", \"date\": \"next Friday\"}}]"}
23
+ {"instruction": "Order large pepperoni pizza", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"order_food\", \"description\": \"Order food\", \"parameters\": {\"type\": \"object\", \"properties\": {\"item\": {\"type\": \"string\"}, \"size\": {\"type\": \"string\"}, \"toppings\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}}}, \"required\": [\"item\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"order_food\", \"arguments\": {\"item\": \"pizza\", \"size\": \"large\", \"toppings\": [\"pepperoni\"]}}]"}
24
+ {"instruction": "Check my bank balance", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"get_balance\", \"description\": \"Get account balance\", \"parameters\": {\"type\": \"object\", \"properties\": {\"account_type\": {\"type\": \"string\", \"enum\": [\"checking\", \"savings\"]}}, \"required\": [\"account_type\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"get_balance\", \"arguments\": {\"account_type\": \"checking\"}}]"}
25
+ {"instruction": "Get weather in Berlin and calculate 100/4", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"get_weather\", \"description\": \"Get weather\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\"}}, \"required\": [\"location\"]}}}, {\"type\": \"function\", \"function\": {\"name\": \"calculate\", \"description\": \"Math\", \"parameters\": {\"type\": \"object\", \"properties\": {\"expression\": {\"type\": \"string\"}}, \"required\": [\"expression\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"get_weather\", \"arguments\": {\"location\": \"Berlin\"}}, {\"name\": \"calculate\", \"arguments\": {\"expression\": \"100/4\"}}]"}
26
+ {"instruction": "Search for restaurants and get weather in Chicago", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"search_web\", \"description\": \"Search\", \"parameters\": {\"type\": \"object\", \"properties\": {\"query\": {\"type\": \"string\"}}, \"required\": [\"query\"]}}}, {\"type\": \"function\", \"function\": {\"name\": \"get_weather\", \"description\": \"Weather\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\"}}, \"required\": [\"location\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"search_web\", \"arguments\": {\"query\": \"restaurants\"}}, {\"name\": \"get_weather\", \"arguments\": {\"location\": \"Chicago\"}}]"}
27
+ {"instruction": "Read file config.json", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"read_file\", \"description\": \"Read a file\", \"parameters\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}}, \"required\": [\"path\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"read_file\", \"arguments\": {\"path\": \"config.json\"}}]"}
28
+ {"instruction": "Write 'hello world' to output.txt", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"write_file\", \"description\": \"Write to file\", \"parameters\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}}, \"required\": [\"path\", \"content\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"write_file\", \"arguments\": {\"path\": \"output.txt\", \"content\": \"hello world\"}}]"}
29
+ {"instruction": "Run the tests", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"run_command\", \"description\": \"Run shell command\", \"parameters\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}}, \"required\": [\"command\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"run_command\", \"arguments\": {\"command\": \"npm test\"}}]"}
30
+ {"instruction": "List files in current directory", "input": "[AVAILABLE_TOOLS] [{\"type\": \"function\", \"function\": {\"name\": \"list_files\", \"description\": \"List files in directory\", \"parameters\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}}, \"required\": [\"path\"]}}}] [/AVAILABLE_TOOLS]", "output": "[TOOL_CALLS] [{\"name\": \"list_files\", \"arguments\": {\"path\": \".\"}}]"}
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Demo: Fine-tune SigLIP2 for image classification
5
+ # This shows the full workflow with actual weight updates
6
+
7
+ require "bundler/setup"
8
+ require "fine"
9
+
10
+ puts "=" * 60
11
+ puts "FINE-TUNING DEMO"
12
+ puts "=" * 60
13
+
14
+ Fine.configure do |config|
15
+ config.progress_bar = false
16
+ end
17
+
18
+ fixtures_path = File.expand_path("../spec/fixtures/images", __dir__)
19
+
20
+ puts "\n[1] Create and configure classifier"
21
+ classifier = Fine::ImageClassifier.new("google/siglip2-base-patch16-224") do |config|
22
+ config.epochs = 5
23
+ config.batch_size = 2
24
+ config.learning_rate = 1e-3 # Higher LR for faster learning on tiny dataset
25
+ config.freeze_encoder = true # Train only classification head
26
+
27
+ config.on_epoch_end do |epoch, metrics|
28
+ puts " Epoch #{epoch + 1}: loss=#{metrics[:loss].round(4)}, acc=#{(metrics[:accuracy] * 100).round(1)}%"
29
+ end
30
+ end
31
+
32
+ puts "\n[2] Train on #{fixtures_path}"
33
+ puts " (4 images: 2 cats, 2 dogs)"
34
+ puts ""
35
+
36
+ history = classifier.fit(train_dir: fixtures_path)
37
+
38
+ puts "\n[3] Training metrics"
39
+ initial_loss = history.first[:loss]
40
+ final_loss = history.last[:loss]
41
+ initial_acc = history.first[:accuracy]
42
+ final_acc = history.last[:accuracy]
43
+
44
+ puts " Initial: loss=#{initial_loss.round(4)}, accuracy=#{(initial_acc * 100).round(1)}%"
45
+ puts " Final: loss=#{final_loss.round(4)}, accuracy=#{(final_acc * 100).round(1)}%"
46
+
47
+ if final_loss < initial_loss
48
+ puts " ✓ Loss decreased by #{((1 - final_loss/initial_loss) * 100).round(1)}%"
49
+ else
50
+ puts " ⚠ Loss did not decrease"
51
+ end
52
+
53
+ if final_acc > initial_acc
54
+ puts " ✓ Accuracy improved from #{(initial_acc * 100).round(1)}% to #{(final_acc * 100).round(1)}%"
55
+ end
56
+
57
+ puts "\n[4] Test predictions"
58
+ Dir.glob(File.join(fixtures_path, "*/*.jpg")).each do |image_path|
59
+ true_label = File.basename(File.dirname(image_path))
60
+ predictions = classifier.predict(image_path).first
61
+ pred_label = predictions.first[:label]
62
+ pred_score = predictions.first[:score]
63
+
64
+ status = pred_label == true_label ? "✓" : "✗"
65
+ puts " #{status} #{File.basename(image_path)}: predicted=#{pred_label} (#{(pred_score * 100).round(1)}%), actual=#{true_label}"
66
+ end
67
+
68
+ puts "\n[5] Save and reload model"
69
+ save_path = "/tmp/fine_demo_model"
70
+ classifier.save(save_path)
71
+ puts " Saved to: #{save_path}"
72
+
73
+ loaded = Fine::ImageClassifier.load(save_path)
74
+ puts " Loaded successfully with #{loaded.label_map.size} classes: #{loaded.label_map.keys.join(', ')}"
75
+
76
+ puts "\n" + "=" * 60
77
+ puts "DEMO COMPLETE"
78
+ puts "=" * 60
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Fine-tune Gemma 3 (1B) for tool calling
5
+ #
6
+ # This script fine-tunes Gemma 3 1B instruction-tuned model to generate
7
+ # tool calls in Ollama-compatible format.
8
+ #
9
+ # Format:
10
+ # Input: [AVAILABLE_TOOLS] [{...tool definitions...}] [/AVAILABLE_TOOLS]
11
+ # Output: [TOOL_CALLS] [{...tool calls...}]
12
+
13
+ require "bundler/setup"
14
+ require "fine"
15
+
16
+ puts "=" * 60
17
+ puts "GEMMA 3 TOOL CALLING FINE-TUNING"
18
+ puts "=" * 60
19
+
20
+ Fine.configure do |config|
21
+ config.progress_bar = false
22
+ end
23
+
24
+ data_path = File.expand_path("data/tool_calls.jsonl", __dir__)
25
+
26
+ # Gemma 3 1B instruction-tuned
27
+ model_id = "google/gemma-3-1b-it"
28
+
29
+ puts "\n1. Setting up LLM with #{model_id}..."
30
+ puts " (This will download ~2GB from HuggingFace if not cached)"
31
+
32
+ begin
33
+ llm = Fine::LLM.new(model_id) do |config|
34
+ config.epochs = 3
35
+ config.batch_size = 1 # Reduced for memory
36
+ config.learning_rate = 2e-5
37
+ config.max_length = 256 # Reduced for memory
38
+ config.gradient_accumulation_steps = 1 # Set to 1 to avoid memory issues
39
+ config.warmup_steps = 10
40
+ config.max_grad_norm = nil # Disable gradient clipping to simplify
41
+
42
+ config.on_epoch_end do |epoch, metrics|
43
+ puts " Epoch #{epoch}: loss=#{metrics[:loss].round(4)}"
44
+ end
45
+ end
46
+
47
+ puts " Config:"
48
+ puts " Epochs: #{llm.config.epochs}"
49
+ puts " Batch size: #{llm.config.batch_size}"
50
+ puts " Learning rate: #{llm.config.learning_rate}"
51
+ puts " Max length: #{llm.config.max_length}"
52
+
53
+ puts "\n2. Loading training data from #{data_path}..."
54
+ line_count = File.readlines(data_path).count
55
+ puts " Found #{line_count} examples"
56
+
57
+ puts "\n3. Starting fine-tuning..."
58
+ puts " (This may take a while depending on your hardware)"
59
+ puts ""
60
+
61
+ history = llm.fit(train_file: data_path, format: :alpaca)
62
+
63
+ puts "\n4. Training completed!"
64
+ puts " Final loss: #{history.last[:loss].round(4)}"
65
+
66
+ if history.size >= 2 && history.last[:loss] < history.first[:loss]
67
+ improvement = ((1 - history.last[:loss] / history.first[:loss]) * 100).round(1)
68
+ puts " Loss improved by #{improvement}%"
69
+ end
70
+
71
+ puts "\n5. Testing tool call generation..."
72
+
73
+ test_prompts = [
74
+ {
75
+ instruction: "What's the weather in Chicago?",
76
+ tools: "Available tools:\n- get_weather(location: string) - Get current weather\n- calculate(expression: string) - Evaluate math"
77
+ },
78
+ {
79
+ instruction: "Calculate 50 + 25 * 2",
80
+ tools: "Available tools:\n- get_weather(location: string) - Get current weather\n- calculate(expression: string) - Evaluate math"
81
+ },
82
+ {
83
+ instruction: "Search for Python tutorials",
84
+ tools: "Available tools:\n- search_web(query: string) - Search the web\n- get_weather(location: string) - Get weather"
85
+ }
86
+ ]
87
+
88
+ test_prompts.each do |prompt|
89
+ full_prompt = "### Instruction:\n#{prompt[:instruction]}\n\n### Input:\n#{prompt[:tools]}\n\n### Response:\n"
90
+
91
+ response = llm.generate(
92
+ full_prompt,
93
+ max_new_tokens: 100,
94
+ temperature: 0.1, # Low temperature for deterministic tool calls
95
+ do_sample: false # Greedy decoding for consistent output
96
+ )
97
+
98
+ # Extract just the response part
99
+ generated = response.split("### Response:").last.strip
100
+
101
+ puts "\n Prompt: \"#{prompt[:instruction]}\""
102
+ puts " Generated:"
103
+ puts " #{generated.lines.first(5).map(&:strip).join("\n ")}"
104
+ end
105
+
106
+ puts "\n6. Saving fine-tuned model..."
107
+ save_path = "/tmp/gemma3-tool-calling"
108
+ llm.save(save_path)
109
+ puts " Saved to: #{save_path}"
110
+
111
+ puts "\n7. Testing load and generate..."
112
+ loaded = Fine::LLM.load(save_path)
113
+
114
+ test_prompt = "### Instruction:\nCheck the temperature in Boston\n\n### Input:\nAvailable tools:\n- get_weather(location: string) - Get weather\n\n### Response:\n"
115
+ loaded_response = loaded.generate(test_prompt, max_new_tokens: 50, do_sample: false)
116
+ generated = loaded_response.split("### Response:").last.strip
117
+
118
+ puts " Loaded model response:"
119
+ puts " #{generated.lines.first(3).map(&:strip).join("\n ")}"
120
+
121
+ puts "\n" + "=" * 60
122
+ puts "GEMMA 3 TOOL CALLING FINE-TUNING COMPLETE!"
123
+ puts "=" * 60
124
+ puts "\nModel saved to: #{save_path}"
125
+ puts "You can load it with: Fine::LLM.load('#{save_path}')"
126
+
127
+ rescue => e
128
+ puts "\n" + "=" * 60
129
+ puts "FINE-TUNING FAILED!"
130
+ puts "=" * 60
131
+ puts "\nError: #{e.class}: #{e.message}"
132
+ puts "\nBacktrace:"
133
+ puts e.backtrace.first(20).join("\n")
134
+ exit 1
135
+ end